/** mopsdiffcor -- MOPSish JPL differential corrector

This version of the JPL differential corrector handles batch
tracks and reads in MOPS MITI format files.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <search.h>         // GNU hash routines

#include <miti.h>
#include "mopsod.h"


/** Forward declarations. */
int get_iod(ORBIT_BLK *orb, char *line);


/** Globals. */
#define DIE_MSG_LEN 249
static char die_msg[DIE_MSG_LEN];      ///< last-ditch string buff

static int debug = 0;                  ///< enable debug mode
static int verbose = 0;                ///< lotsa output

/** DES_OBSERVATION_STRUCT
 *
 * In-memory representation of DES observation. */
typedef struct {
    char *id;
    double epoch_mjd;
    double ra_deg;
    double dec_deg;
    double mag;
    char obscode[MITI_OBSCODE_LEN + 1];
    double ra_sigma_arcsec;
    double dec_sigma_arcsec;
    double mag_sigma;
    char object_name[MITI_GROUND_LEN + 1];
} DES_OBSERVATION_STRUCT;


/** Error codes */
enum {
    EMOPSDIFFCOR_ERROROPENINGIOD = 110,
    EMOPSDIFFCOR_ERROROPENINGOBS,
    EMOPSDIFFCOR_BADTRACKDATA,
    EMOPSDIFFCOR_HCREATEFAILED,
    EMOPSDIFFCOR_NOMEM,
    EMOPSDIFFCOR_UNKNOWN = 199
};


/** fail
print err msg and exit
*/
void fail(int code, char *msg){
    fprintf(stderr, msg);
    exit(code);
}


/** Display usage message and quit.
*/
void usage(void) {
    fprintf(stderr, "\
Usage: mopsdiffcor [options] IODS OBSERVATIONS ORBITS\n\
\n\
  --report_all : emit error results and out-of-range corrections\n\
  --covariance : append covariance elements\n\
  --threshold_arcseconds : residual threshold for acceptance\n\
  --verbose : verbose output\n\
  --debug : enable debug output\n\
  IODS : input IOD file\n\
  OBSERVATIONS : input observations file\n\
  ORBITS : output file\n\
\n\
");
}


/** read_iods
*/
int read_iods(char *filename, ORBIT_BLK **iod_blkptr, int *num_iodsptr) {

    /** Allocate buffer for IODs.  If we read more than is here, the
    we'll increment the block with realloc(). */
    int max_iods = 5000;

    ORBIT_BLK *iods = (ORBIT_BLK *) malloc(max_iods * sizeof(ORBIT_BLK));
    if (NULL == iods) {
        fail(EMOPSDIFFCOR_NOMEM, "Can't alloc iods.\n");
    }

    FILE *fp;
    if (strcmp(filename, "-") == 0) {
        ///< STDIN specified.
        fp = stdin;
    }
    else {
        fp = fopen(filename, "r");
        if (NULL == fp) {
            fail(EMOPSDIFFCOR_ERROROPENINGIOD,  "Can't open input IOD file.");
        }
    }

    fseek(fp, 0, SEEK_SET);                     ///< rewind to beginning
    int norb = 0;                               ///< initialize number of lines found
    char line[5000];
    while (fgets(line, 5000, fp)) {             ///< get next line from file
        /** Check to see if we've exceeded the current size of our buffer.
        If so, realloc() it. */
        if (norb >= max_iods) {
            max_iods *= 2;
            iods = (ORBIT_BLK *) realloc(iods, max_iods * sizeof(ORBIT_BLK));
            if (NULL == iods) {
                snprintf(die_msg, DIE_MSG_LEN, "Can't realloc iods to %ld.\n", max_iods * sizeof(ORBIT_BLK));
                fail(EMOPSDIFFCOR_NOMEM, die_msg);
            }
        }

//        if (line[0] != '#' && line[0] != 'E') { ///< not comment/error line
        if (strncmp(line, "MIF-O", 5) == 0) { 
            get_iod(&iods[norb], line);         ///< parse it
            norb++;
        }
    }

    if (verbose)
        fprintf(stderr, "Found %d IODs\n", norb);


    ///< Clean up.
    *num_iodsptr = norb;
    *iod_blkptr = iods;
    fclose(fp);         ///< all done

    return 0;
}


void _dump_orb(FILE *f, ORBIT_BLK *orb) {
    fprintf(f, "%s %f %f %f %f %f %f %f %f %f %d\n",
        orb->id,
        orb->q_au,
        orb->e,
        orb->i_deg,
        orb->node_deg,
        orb->arg_peri_deg,
        orb->time_peri_mjd,
        orb->h_v,
        orb->t_epoch_mjd,
        orb->residual_arcseconds,
        orb->ccode                            ///< JPL od_converge() code
    );
}


void _dump_obs(FILE *f, OBSERVATION_STRUCT *obs) {
    fprintf(f, "%s %f %f %f %f %s %f %f %f\n",
        obs->id,
        obs->epoch_mjd,
        obs->ra_deg,
        obs->dec_deg,
        obs->mag,
        obs->obscode,
        obs->ra_sigma_arcsec,
        obs->dec_sigma_arcsec,
        obs->mag_sigma
    );
}



void _dump_all(ORBIT_BLK *orb, int norb, struct hsearch_data *hs) {
    /** Just dump stuff. */
    int i;
    int j;
    ENTRY *e, orb_e;
    for (i = 0; i < norb; i++) {
        _dump_orb(stderr, orb + i);
        orb_e.key = orb[i].id;
        hsearch_r(orb_e, FIND, &e, hs);
        if (e) {
            OBSERVATION_BLK *hdr = (OBSERVATION_BLK *) e->data;
            for (j = 0; j < hdr->nobs; j++) {
                _dump_obs(stderr, &hdr->obs[j]);
            }
        }
    }
}


/** read_observations
*/
int read_observations(char *filename, struct hsearch_data *obs_tab) {
    FILE *fp = fopen(filename, "r");
    if (NULL == fp)
        fail(EMOPSDIFFCOR_UNKNOWN, "Can't open OBS input file.\n");        ///< couldn't open file.

    /** Count lines in file. */
    char line[5000];
    int nlines = 0;
    int len;
    while (fgets(line, 5000, fp)) {
        len = strlen(line);
        if (len > 4000) {
            snprintf(die_msg, DIE_MSG_LEN, "Line %d too long.\n", nlines);
            fail(EMOPSDIFFCOR_UNKNOWN, die_msg);
        }
        if (line[0] != '#' && line[0] != 'E')
            nlines++;
    }

    /** Allocate buffers for obs stuff. */
    memset(obs_tab, 0, sizeof(*obs_tab));
    if (!hcreate_r(nlines * 2, obs_tab)) {
        fail(EMOPSDIFFCOR_HCREATEFAILED, "hcreate() failed.\n");
    }

    ///< Master observation list, allocated for worst-case (1 ID/1 obs)
    int trk_i = -1;      ///< index of current track
    OBSERVATION_BLK *trk_list = (OBSERVATION_BLK *) malloc(nlines * sizeof(OBSERVATION_BLK));
    if (NULL == trk_list) {
        fail(EMOPSDIFFCOR_UNKNOWN, "Can't alloc obs header.\n");
    }

    int obs_i = 0;
    OBSERVATION_STRUCT *obs_list = (OBSERVATION_STRUCT *) calloc(nlines, sizeof(OBSERVATION_STRUCT));
    if (NULL == obs_list) {
        fail(EMOPSDIFFCOR_UNKNOWN, "Can't alloc obs list.\n");
    }

    DES_OBSERVATION_STRUCT des_obs;
    char last_id[ORBIT_ID_LEN + 1] = "###";     ///< ID of last processed obs line ('###' == dummy)

    fseek(fp, 0, SEEK_SET);         ///< rewind to beginning
    while (fgets(line, 5000, fp)) {
        if (line[0] == '!' || line[0] == '#')
            continue;               ///< first line is comment, skip

        ///< Load observation.
        if (get_des_observation(line, &des_obs)) {
            snprintf(die_msg, DIE_MSG_LEN, "Error reading DES observation line: %s\n", line);
            fail(EMOPSDIFFCOR_UNKNOWN, die_msg);
        }

        ///< Initialize orbit struct.
        OBSERVATION_STRUCT *obs = &obs_list[obs_i]; ///< shorthand
        strncpy(obs->id, des_obs.id, ORBIT_ID_LEN);
        obs->epoch_mjd = des_obs.epoch_mjd;
        obs->ra_deg = des_obs.ra_deg;
        obs->dec_deg = des_obs.dec_deg;
        obs->mag = des_obs.mag;
        strncpy(obs->obscode, des_obs.obscode, ORBIT_OBSCODE_LEN);
        obs->ra_sigma_arcsec = des_obs.ra_sigma_arcsec;
        obs->dec_sigma_arcsec = des_obs.dec_sigma_arcsec;
        obs->mag_sigma = des_obs.mag_sigma;

        if (strcmp(last_id, obs->id) != 0) {
            /** IDs differ, so initialize a new hash item with this track ID.
            As a safety check, see if this ID already exists.  If it does, it
            means the the input data was not sorted, so bail.  */
            ENTRY *e;
            ENTRY obs_e;
            obs_e.key = obs->id;        ///< ID
            obs_e.data = obs;           ///< ptr to obs
            hsearch_r(obs_e, FIND, &e, obs_tab);
            if (NULL == e) {
                ///< new
                trk_i++;                ///< new track index
                trk_list[trk_i].nobs = 1;
                trk_list[trk_i].obs = obs;
                obs_e.data = &trk_list[trk_i];
                hsearch_r(obs_e, ENTER, &e, obs_tab);  ///< insert into hash
                strncpy(last_id, obs->id, ORBIT_ID_LEN);
            }
            else {
                snprintf(die_msg, DIE_MSG_LEN, "Found disconnected observation: %s\n", line);
                fail(EMOPSDIFFCOR_UNKNOWN, die_msg);
            }
        }
        else {  
            /** Current ID is same as last, so we're just adding.  Update the count
            of observations for this track. */
            trk_list[trk_i].nobs++;
        }

        obs_i++;            ///< next obs slot in big list
    }

    ///< Clean up.
    fclose(fp);         ///< all done

    return 0;
}


/** Parse command-line options
*/
static int get_options(
    int argc,
    char **argv,
    int *debug,
    int *verbose,
    double *fallback_iod,
    int *report_all,
    int *write_covariance,
    double *threshold_arcseconds,
    char **iodfilename,
    char **obsfilename,
    char **outfilename
) {
    int c;
    int option_index = 0;
    while (1) {
        static struct option long_opts[] = {
            { "debug", no_argument, NULL, 0},
            { "verbose", no_argument, NULL, 0},
            { "fallback_iod", required_argument, NULL, 0},
            { "covariance", no_argument, NULL, 0},
            { "report_all", no_argument, NULL, 0},
            { "threshold_arcseconds", required_argument, NULL, 0},
            { 0, 0, 0, 0},
        };
        c = getopt_long(argc, argv, "", long_opts, &option_index);
        if (c == -1)
            break;

        switch (c) {
            case 0:
                if (strcmp(long_opts[option_index].name, "verbose") == 0) {
                    *verbose = 1;
                }
                if (strcmp(long_opts[option_index].name, "fallback_iod") == 0) {
                    *fallback_iod = atof(optarg);
                }
                if (strcmp(long_opts[option_index].name, "debug") == 0) {
                    *debug = 1;
                }
                else if (strcmp(long_opts[option_index].name, "report_all") == 0) {
                    *report_all = 1;
                }
                else if (strcmp(long_opts[option_index].name, "covariance") == 0) {
                    *write_covariance = 1;
                }
                else if (strcmp(long_opts[option_index].name, "threshold_arcseconds") == 0) {
                    *threshold_arcseconds = atof(optarg);
                }
                break;

            default:
                usage();
                exit(100);
        }
    }

    if (optind < argc) {
        *iodfilename = argv[optind++];
    }
    if (optind < argc) {
        *obsfilename = argv[optind++];
    }
    if (optind < argc) {
        *outfilename = argv[optind++];
    }

    if (NULL == *iodfilename || NULL == *obsfilename || NULL == *outfilename)
        return 1;       ///< something not specified

    return 0;
};


/** Write out orbit to output file
*/
void write_orbit(FILE *outfile, ORBIT_BLK *orb, int write_covariance) {
    char ccode_str[100];
    snprintf(ccode_str, 99, "D%d", orb->ccode); ///< stringy convergence code

    fprintf(outfile, "%s %s %.12f %.12f %.12f %.12f %.12f %.12f %.12f %.12f %.12f %s %s %.10f %s",
        write_covariance ? "MIF-OC" : "MIF-O",
        orb->id,
        orb->q_au,
        orb->e,
        orb->i_deg,
        orb->node_deg,
        orb->arg_peri_deg,
        orb->time_peri_mjd,
        orb->t_epoch_mjd,
        orb->h_v,
        orb->residual_arcseconds,
        "undef",                            ///< chi squared
        "undef",                            ///< moid_1
        orb->arc_length_days,               ///< arcLength_days
        ccode_str                           ///< JPL od_converge() code
    );

    if (write_covariance && orb->cov_valid) {
        // Write covariance elements.
        int i;
        for (i = 0; i < NUM_COVARIANCE; i++) {
            fprintf(outfile, " %e", orb->covariance[i]);
        }
    }

    fprintf(outfile, "\n");
}


/** Check that the orbit contains nominally correct-looking stuff.
*/
int valid_orbit(ORBIT_BLK *orb) {
//    if (orb->q_au < 0 || orb->e > 1 || orb->i_deg < 0 || orb->i_deg > 180)
    if (orb->q_au < 0)
        return 0;

    return 1;           ///< yay!
}


/** MAIN__

Plan is to read all IODs, hash them; read all tracks (groups of
observations), hash them; then for each IOD, look up its observations
and run the corrector on them.  

Note that we don't allow output to STDOUT.  The JPL DIVA libraries
generate all kinds of noise on STDOUT and it's a PITA to filter
it.
*/

/* 
  The Lahey compiler requires the main to be called MAIN__ while the Intel 
  compiler wants it to be called main.
*/
#define __INTEL_COMPILER
#ifdef __INTEL_COMPILER
  int main(int argc, char **argv) {
#else
  int MAIN__(int argc, char **argv) {
#endif
//    OBSERVATION_BLK ob;
//    ORBIT_BLK out;


    FILE *iodfile = NULL;           
    FILE *obsfile = NULL;
    FILE *outfile = NULL;          
    ORBIT_BLK *iod_blk = NULL;
    ORBIT_BLK *out_blk = NULL;
    int num_iods = 0;
    int res;    ///< return code
    struct hsearch_data obs_tab;

    ///< Options.
    char *iodfilename = NULL;       ///< input IOD filename
    char *obsfilename = NULL;       ///< input observation filename
    char *outfilename = NULL;       ///< output filename
    int report_all = 0;                 ///< 1 => report all corrector results; 0 => only 'good' ones
    int write_covariance = 0;           ///< 1 => report covariance elements at end of line
    double fallback_iod = 0;            ///< use IOD if diffcor fails and IOD resid < fallback_iod
    double threshold_arcseconds = 1e10; ///< orbit fit residual threshold, arcseconds



    ///< Handle command-line.
    if (get_options(argc, argv, 
            &debug, 
            &verbose, 
            &fallback_iod, 
            &report_all, 
            &write_covariance, 
            &threshold_arcseconds, 
            &iodfilename, 
            &obsfilename, 
            &outfilename)) {
        usage();
        exit(1);
    }


    ///< Open input files. */
    obsfile = fopen(obsfilename, "r");
    if (NULL == obsfile) {
        fail(EMOPSDIFFCOR_ERROROPENINGOBS,  "Can't open input observations file.\n");
    }
   
    ///< Read IOD IDs and parameters.
    res = read_iods(iodfilename, &iod_blk, &num_iods);
    if (res) fail(res, "Error reading IODs.\n");

    res = read_observations(obsfilename, &obs_tab);
    if (res) fail(res, "Error reading observations.\n");


//    if (debug) {
//        _dump_all(iod_blk, num_iods, &obs_tab);
//        exit(0);
//    }


    ///< Initialize stuff.
    outfile = fopen(outfilename, "w");
    if (NULL == outfile) {
        snprintf(die_msg, DIE_MSG_LEN, "Can't open output file: %s\n", outfilename);
        fail(EMOPSDIFFCOR_UNKNOWN, die_msg);
    }

    ///< Dump options.
    if (debug) {
        fprintf(stderr, "fallback_iod=%f\n", fallback_iod);
    }

    mopsod_init();          ///< JPL lib init


    /** Loop through each of the orbits that we have an IOD for.  If we
    can't find the observations for it, print an error code to STDOUT and
    the output file.  Otherwise send it to the JPL corrector and print out
    the results.  */
    int iod_i;

    if (debug) {
        fprintf(stderr, "About to process %d iods.\n", num_iods);
    }

    for (iod_i = 0; iod_i < num_iods; iod_i++) {
        ///< Look up observation data for this IOD.
        ORBIT_BLK *iod = &iod_blk[iod_i];
        ORBIT_BLK out;
        OBSERVATION_BLK *obs_blk;
        ENTRY obs_e;
        ENTRY *e;

        if (verbose) 
            fprintf(stderr, "Processing: %s\n", iod->id);


        ///< Check for nonsense orbit.
        if (!valid_orbit(iod)) {
            if (report_all)
                fprintf(outfile, "EMOPSDIFFCOR_NONSENSE %s\n", iod->id);
            continue;
        }

        ///< Temporary check on e.
        if (iod->e > 1e4) {
            if (report_all)
                fprintf(outfile, "EMOPSDIFFCOR_E_TOO_LARGE %s: %d\n", iod->id, iod->e);
            continue;
        }

        obs_e.key = iod->id;
        obs_e.data = NULL;
        res = hsearch_r(obs_e, FIND, &e, &obs_tab);           ///< look up list of obs with this ID
        if (NULL == e) {
            ///< didn't find any observations for this ID
            if (report_all)
                fprintf(outfile, "EMOPSDIFFCOR_NOOBS %s\n", iod->id);
            continue;               ///< skip to next IOD
        }
        obs_blk = (OBSERVATION_BLK *) e->data;
        if (NULL == obs_blk) {
            ///< didn't find any observations for this ID
            sprintf(die_msg, "Got empty block for track %s.\n", iod->id);   ///< bad
            fail(EMOPSDIFFCOR_BADTRACKDATA, die_msg);
        }
       

        ///< Everything looks good.  Run the differential correction. 
        res = mopsod_diffcor(&iod_blk[iod_i], obs_blk, &out);
        out.cov_valid = 1;


        ///< Report the result.
//        fprintf(outfile, "RES %d CCODE %d IOD %f DC %f\n", 
//            res, 
//            out.ccode,
//            iod_blk[iod_i].residual_arcseconds,
//            out.residual_arcseconds
//        );

        if (0 == res) {         ///< computation completed successfully
            // ccode == -9 means that none of the reduced-parameterization orbits worked either.
            // ccode == -8 means resid way too large
            if ((out.ccode != -9 && out.ccode != -8) && (report_all || out.residual_arcseconds < threshold_arcseconds))  {
                write_orbit(outfile, &out, write_covariance);
            }
            else if (fallback_iod && iod_blk[iod_i].residual_arcseconds < fallback_iod) {
                iod_blk[iod_i].ccode = 9;              ///< MOPS code for IOD used
                iod_blk[iod_i].arc_length_days = out.arc_length_days;  ///< snake from diffcor orbit
                write_orbit(outfile, &iod_blk[iod_i], 0);
            }
        }
        else {
            if (fallback_iod && iod_blk[iod_i].residual_arcseconds < fallback_iod) {
                iod_blk[iod_i].ccode = 9;              ///< MOPS code for IOD used
                iod_blk[iod_i].arc_length_days = out.arc_length_days;  ///< snake from diffcor orbit
                write_orbit(outfile, &iod_blk[iod_i], 0);
            }
            else if (report_all) {
                fprintf(outfile, "EMOPSDIFFCOR_JPLFAIL %s %d\n", iod->id, res);  ///< unknown JPL failure
            }
        }
    }

    fclose(outfile);
    exit(0);
}


/** get_des_detection - parse a DES detection line.
return zero if no errors occured.
*/
int get_des_observation(char *buf, DES_OBSERVATION_STRUCT *blk) {
    char *tok;
    char *last;
    #define DES_ITEMS 13
    char *tokens[DES_ITEMS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    int num_toks = 0;
    int n = 0;

    if (NULL == blk)
        return EMITIBADINPUT;

    /* Tokenize line into array of (char *). */
    tok = strtok_r(buf, " 	", &last);
    while (tok != NULL && num_toks < DES_ITEMS) {
        tokens[num_toks] = tok;
        tok = strtok_r(NULL, " 	", &last);
        num_toks++;
    }

    /* Parse the interesting tokens. */
    n = 0;

    if (tokens[n]) {
        blk->id = (char*) malloc(strlen(tokens[n]) + 1);
        strcpy(blk->id, tokens[n]);
    }
    else
        return EMITIINSUFFARGS;
    n++;

    if (tokens[n]) 
        blk->epoch_mjd = atof(tokens[n]);
    else
        return EMITIINSUFFARGS;
    n++;

    n++;            /* skip obs type */

    if (tokens[n]) 
        blk->ra_deg = atof(tokens[n]);
    else
        return EMITIINSUFFARGS;
    n++;

    if (tokens[n]) 
        blk->dec_deg = atof(tokens[n]);
    else
        return EMITIINSUFFARGS;
    n++;

    if (tokens[n]) 
        blk->mag = atof(tokens[n]);
    else
        return EMITIINSUFFARGS;
    n++;

    n++;    /* skip filter (for now) */

    if (tokens[n]) 
        strncpy(blk->obscode, tokens[n], 9);
    else
        return EMITIINSUFFARGS;
    n++;

    if (tokens[n]) 
        blk->ra_sigma_arcsec = atof(tokens[n]);
    else
        return EMITIINSUFFARGS;
    n++;

    if (tokens[n]) 
        blk->dec_sigma_arcsec = atof(tokens[n]);
    else
        return EMITIINSUFFARGS;
    n++;

    if (tokens[n]) 
        blk->mag_sigma = atof(tokens[n]);
    else
        return EMITIINSUFFARGS;
    n++;

    n++;    /* skip S/N */

    if (tokens[n]) 
        strncpy(blk->object_name, tokens[n], 19);
    else
        blk->object_name[0] = 0;     ///< empty string
    n++;

    return 0;
}



/** get_iod()
Tokenize a line read in using fgets into orbital elements.  We'll be
fetching the values:
  MIF-O prefix
  orbit ID
  q, perihelion distance, in AU
  e, eccentricity
  i, inclination
  Om, longitude of ascending node, deg
  w, argument of perihelion, deg
  tp, time of perihelion, MJD
  epoch, MJD
*/
int get_iod(ORBIT_BLK *orb, char *line) {
    char *tok;
    char *delim = " \t\n";


    tok = strtok(line, delim);
    if (NULL == tok)
        return 1;
    else if (strncmp(tok, "MIF-O", 5) != 0) {
        return 1;       // wrong prefix
    }

    // orbit ID
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        strncpy(orb->id, tok, ORBIT_ID_LEN);

    // q
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        orb->q_au = atof(tok);

    // e
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        orb->e = atof(tok);

    // i
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        orb->i_deg = atof(tok);

    // node
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        orb->node_deg = atof(tok);

    // argPeri
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        orb->arg_peri_deg = atof(tok);

    // timePeri
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        orb->time_peri_mjd = atof(tok);

    // epoch
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        orb->t_epoch_mjd = atof(tok);

    // mag
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        orb->h_v = atof(tok);

    // residual
    tok = strtok(NULL, delim);
    if (NULL == tok)
        return 1;
    else
        orb->residual_arcseconds = atof(tok);


    // Other initializations.
    orb->arc_length_days = 0;       // default
    orb->ccode = 0;
    orb->cov_valid = 0;
    return 0;   // OK
}
