/******************************************************************************
 *
 * Project:  MSG Native Reader
 * Purpose:  All code for EUMETSAT Archive format reader
 * Author:   Frans van den Bergh, fvdbergh@csir.co.za
 *
 ******************************************************************************
 * Copyright (c) 2005, Frans van den Bergh <fvdbergh@csir.co.za>
 * Copyright (c) 2008-2009, Even Rouault <even dot rouault at spatialys.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

#include "gdal_frmts.h"
#include "gdal_priv.h"
#include "ogr_spatialref.h"

#include "msg_reader_core.h"

#include <algorithm>

using namespace msg_native_format;

CPL_CVSID("$Id$")

typedef enum {
    MODE_VISIR,     // Visible and Infrared bands (1 through 11) in 10-bit raw mode
    MODE_HRV,       // Pan band (band 11) only, in 10-bit raw mode
    MODE_RAD     // Black-body temperature (K) for thermal bands only (4-10), 64-bit float
    } open_mode_type;

class MSGNRasterBand;

/************************************************************************/
/* ==================================================================== */
/*                            MSGNDataset                               */
/* ==================================================================== */
/************************************************************************/

class MSGNDataset final: public GDALDataset
{
    friend class MSGNRasterBand;

    VSILFILE       *fp;

    Msg_reader_core*    msg_reader_core;
    double adfGeoTransform[6];
    char   *pszProjection;

  public:
        MSGNDataset();
               ~MSGNDataset();

    static GDALDataset *Open( GDALOpenInfo * );

    CPLErr     GetGeoTransform( double * padfTransform ) override;
    const char *_GetProjectionRef() override;
    const OGRSpatialReference* GetSpatialRef() const override {
        return GetSpatialRefFromOldGetProjectionRef();
    }
};

/************************************************************************/
/* ==================================================================== */
/*                            MSGNRasterBand                            */
/* ==================================================================== */
/************************************************************************/

class MSGNRasterBand final: public GDALRasterBand
{
    friend class MSGNDataset;

    unsigned int packet_size;
    unsigned int bytes_per_line;
    unsigned int interline_spacing;
    unsigned int orig_band_no;      // The name of the band
    unsigned int band_in_file;      // The effective index of the band in the file
    open_mode_type open_mode;

    double  GetNoDataValue (int *pbSuccess=nullptr) override {
        if (pbSuccess) {
            *pbSuccess = 1;
        }
        return MSGN_NODATA_VALUE;
    }

    double MSGN_NODATA_VALUE;

    char band_description[30];

  public:

        MSGNRasterBand( MSGNDataset *, int , open_mode_type mode, int orig_band_no, int band_in_file);

    virtual CPLErr IReadBlock( int, int, void * ) override;
    virtual double GetMinimum( int *pbSuccess = nullptr ) override;
    virtual double GetMaximum(int *pbSuccess = nullptr ) override;
    virtual const char* GetDescription() const override { return band_description; }
};

/************************************************************************/
/*                           MSGNRasterBand()                            */
/************************************************************************/

MSGNRasterBand::MSGNRasterBand( MSGNDataset *poDSIn, int nBandIn,
                                open_mode_type mode, int orig_band_noIn,
                                int band_in_fileIn ) :
    packet_size(0),
    bytes_per_line(0),
    interline_spacing(poDSIn->msg_reader_core->get_interline_spacing()),
    orig_band_no(orig_band_noIn),
    band_in_file(band_in_fileIn),
    open_mode(mode)
{
    poDS = poDSIn;
    nBand = nBandIn;  // GDAL's band number, i.e. always starts at 1.

    snprintf(band_description, sizeof(band_description),
             "band %02u", orig_band_no);

    if( mode != MODE_RAD )
    {
        eDataType = GDT_UInt16;
        MSGN_NODATA_VALUE = 0;
    }
    else
    {
        eDataType = GDT_Float64;
        MSGN_NODATA_VALUE = -1000;
    }

    nBlockXSize = poDS->GetRasterXSize();
    nBlockYSize = 1;

    if( mode != MODE_HRV )
    {
        packet_size = poDSIn->msg_reader_core->get_visir_packet_size();
        bytes_per_line = poDSIn->msg_reader_core->get_visir_bytes_per_line();
    }
    else
    {
        packet_size = poDSIn->msg_reader_core->get_hrv_packet_size();
        bytes_per_line = poDSIn->msg_reader_core->get_hrv_bytes_per_line();
    }
}

/************************************************************************/
/*                             IReadBlock()                             */
/************************************************************************/

CPLErr MSGNRasterBand::IReadBlock( CPL_UNUSED int nBlockXOff,
                                   int nBlockYOff,
                                   void * pImage )

{
    MSGNDataset *poGDS = (MSGNDataset *) poDS;

    // invert y position
    int i_nBlockYOff = poDS->GetRasterYSize() - 1 - nBlockYOff;

    unsigned int data_length =  bytes_per_line + (unsigned int)sizeof(SUB_VISIRLINE);
    vsi_l_offset data_offset = 0;

    if (open_mode != MODE_HRV) {
        data_offset = poGDS->msg_reader_core->get_f_data_offset() +
            static_cast<vsi_l_offset>(interline_spacing)*i_nBlockYOff  + (band_in_file-1)*packet_size +
            (packet_size - data_length);
    } else {
        data_offset = poGDS->msg_reader_core->get_f_data_offset() +
            static_cast<vsi_l_offset>(interline_spacing)*(int(i_nBlockYOff/3) + 1) -
            packet_size*(3 - (i_nBlockYOff % 3)) + (packet_size - data_length);
    }

    if( VSIFSeekL( poGDS->fp, data_offset, SEEK_SET ) != 0 )
        return CE_Failure;

    char *pszRecord = (char *) CPLMalloc(data_length);
    size_t nread = VSIFReadL( pszRecord, 1, data_length, poGDS->fp );

    SUB_VISIRLINE* p = (SUB_VISIRLINE*) pszRecord;
    to_native(*p);

    if (p->lineValidity != 1) {
        for (int c=0; c < nBlockXSize; c++) {
            if (open_mode != MODE_RAD) {
                ((GUInt16 *)pImage)[c] = (GUInt16)MSGN_NODATA_VALUE;
            } else {
                ((double *)pImage)[c] = MSGN_NODATA_VALUE;
            }
        }
    }

    if ( nread != data_length ||
        ( open_mode != MODE_HRV && (p->lineNumberInVisirGrid - poGDS->msg_reader_core->get_line_start()) != (unsigned int)i_nBlockYOff )
       ) { // no sophisticated checking for HRV at the moment
        CPLFree( pszRecord );

        CPLError( CE_Failure, CPLE_AppDefined, "MSGN Scanline corrupt." );

        return CE_Failure;
    }

    // unpack the 10-bit values into 16-bit unsigned short ints
    unsigned char *cptr = (unsigned char*)pszRecord +
        (data_length - bytes_per_line);
    int bitsLeft = 8;

    if (open_mode != MODE_RAD) {
        for (int c=0; c < nBlockXSize; c++) {
            unsigned short value = 0;
            for (int bit=0; bit < 10; bit++) {
                value <<= 1;
                if (*cptr & 128) {
                    value |= 1;
                }
                *cptr <<= 1;
                bitsLeft--;
                if (bitsLeft == 0) {
                    cptr++;
                bitsLeft = 8;
                }
            }
            ((GUInt16 *)pImage)[nBlockXSize-1 - c] = value;
        }
    } else {
        // radiance mode
        for (int c=0; c < nBlockXSize; c++) {
            unsigned short value = 0;
            for (int bit=0; bit < 10; bit++) {
                value <<= 1;
                if (*cptr & 128) {
                    value |= 1;
                }
                *cptr <<= 1;
                bitsLeft--;
                if (bitsLeft == 0) {
                    cptr++;
                bitsLeft = 8;
                }
            }
            double dvalue = double(value);
            double bbvalue = dvalue * poGDS->msg_reader_core->get_calibration_parameters()[orig_band_no-1].cal_slope +
                poGDS->msg_reader_core->get_calibration_parameters()[orig_band_no-1].cal_offset;

            ((double *)pImage)[nBlockXSize-1 -c] = bbvalue;
        }
    }
    CPLFree( pszRecord );
    return CE_None;
}

/************************************************************************/
/*                             GetMinimum()                             */
/************************************************************************/
double MSGNRasterBand::GetMinimum( int *pbSuccess ) {
    if (pbSuccess) {
        *pbSuccess = 1;
    }
    return open_mode != MODE_RAD ? 1 : GDALRasterBand::GetMinimum(pbSuccess);
}

/************************************************************************/
/*                             GetMaximum()                             */
/************************************************************************/
double MSGNRasterBand::GetMaximum(int *pbSuccess ) {
    if (pbSuccess) {
        *pbSuccess = 1;
    }
    return open_mode != MODE_RAD ? 1023 : GDALRasterBand::GetMaximum(pbSuccess);
}

/************************************************************************/
/* ==================================================================== */
/*                             MSGNDataset                             */
/* ==================================================================== */
/************************************************************************/

MSGNDataset::MSGNDataset() :
    fp(nullptr),
    msg_reader_core(nullptr),
    pszProjection(CPLStrdup(""))
{
    std::fill_n(adfGeoTransform, CPL_ARRAYSIZE(adfGeoTransform), 0);
}

/************************************************************************/
/*                            ~MSGNDataset()                             */
/************************************************************************/

MSGNDataset::~MSGNDataset()

{
    if( fp != nullptr )
        VSIFCloseL( fp );

    if (msg_reader_core) {
        delete msg_reader_core;
    }

    CPLFree(pszProjection);
}

/************************************************************************/
/*                          GetGeoTransform()                           */
/************************************************************************/

CPLErr MSGNDataset::GetGeoTransform( double * padfTransform )

{

    for (int i=0; i < 6; i++) {
        padfTransform[i] = adfGeoTransform[i];
    }

    return CE_None;
}

/************************************************************************/
/*                          GetProjectionRef()                          */
/************************************************************************/

const char *MSGNDataset::_GetProjectionRef()

{
    return pszProjection;
}

/************************************************************************/
/*                                Open()                                */
/************************************************************************/

GDALDataset *MSGNDataset::Open( GDALOpenInfo * poOpenInfo )

{
    open_mode_type open_mode = MODE_VISIR;
    GDALOpenInfo* open_info = poOpenInfo;

    if (!poOpenInfo->bStatOK) {
        if ( STARTS_WITH_CI(poOpenInfo->pszFilename, "HRV:") ) {
            open_info = new GDALOpenInfo(&poOpenInfo->pszFilename[4], poOpenInfo->eAccess);
            open_mode = MODE_HRV;
        } else
        if ( STARTS_WITH_CI(poOpenInfo->pszFilename, "RAD:") ) {
            open_info = new GDALOpenInfo(&poOpenInfo->pszFilename[4], poOpenInfo->eAccess);
            open_mode = MODE_RAD;
        }
    }

/* -------------------------------------------------------------------- */
/*      Before trying MSGNOpen() we first verify that there is at        */
/*      least one "\n#keyword" type signature in the first chunk of     */
/*      the file.                                                       */
/* -------------------------------------------------------------------- */
    if( open_info->fpL == nullptr || open_info->nHeaderBytes < 50 ) {
        if (open_info != poOpenInfo) {
            delete open_info;
        }
        return nullptr;
    }

    /* check if this is a "NATIVE" MSG format image */
    if( !STARTS_WITH_CI((char *)open_info->pabyHeader, "FormatName                  : NATIVE") )
    {
        if (open_info != poOpenInfo) {
            delete open_info;
        }
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Confirm the requested access is supported.                      */
/* -------------------------------------------------------------------- */
    if( poOpenInfo->eAccess == GA_Update )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                  "The MSGN driver does not support update access to existing"
                  " datasets.\n" );
        if (open_info != poOpenInfo) {
            delete open_info;
        }
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Create a corresponding GDALDataset.                             */
/* -------------------------------------------------------------------- */
    VSILFILE* fp = VSIFOpenL( open_info->pszFilename, "rb" );
    if( fp == nullptr ) {
        if (open_info != poOpenInfo) {
            delete open_info;
        }
        return nullptr;
    }

    MSGNDataset *poDS = new MSGNDataset();

    poDS->fp = fp;

/* -------------------------------------------------------------------- */
/*      Read the header.                                                */
/* -------------------------------------------------------------------- */
    // first reset the file pointer, then hand over to the msg_reader_core
    CPL_IGNORE_RET_VAL(VSIFSeekL( poDS->fp, 0, SEEK_SET ));

    poDS->msg_reader_core = new Msg_reader_core(poDS->fp);

    if (!poDS->msg_reader_core->get_open_success()) {
        if (open_info != poOpenInfo) {
            delete open_info;
        }
        delete poDS;
        return nullptr;
    }

    poDS->nRasterXSize = poDS->msg_reader_core->get_columns();
    poDS->nRasterYSize = poDS->msg_reader_core->get_lines();

    if (open_mode == MODE_HRV) {
        poDS->nRasterXSize *= 3;
        poDS->nRasterYSize *= 3;
    }

/* -------------------------------------------------------------------- */
/*      Create band information objects.                                */
/* -------------------------------------------------------------------- */
    unsigned int i;
    unsigned int band_count = 1;
    unsigned int missing_band_count = 0;
    const unsigned char* bands = poDS->msg_reader_core->get_band_map();
    unsigned char band_map[MSG_NUM_CHANNELS+1];   // map GDAL band numbers to MSG channels
    for (i=0; i < MSG_NUM_CHANNELS; i++) {
        if (bands[i]) {
            bool ok_to_add = false;
            switch (open_mode) {
                case MODE_VISIR:
                    ok_to_add = i < MSG_NUM_CHANNELS - 1;
                    break;
                case MODE_RAD:
                    ok_to_add = (i <= 2) || (Msg_reader_core::Blackbody_LUT[i+1].B != 0);
                    break;
                case MODE_HRV:
                    ok_to_add = i == MSG_NUM_CHANNELS - 1;
                    break;
            }
            if (ok_to_add) {
                poDS->SetBand( band_count, new MSGNRasterBand( poDS, band_count, open_mode, i+1, i+1 - missing_band_count));
                band_map[band_count] = (unsigned char) (i+1);
                band_count++;
            }
        } else {
            missing_band_count++;
        }
    }

    double pixel_gsd_x;
    double pixel_gsd_y;
    double origin_x;
    double origin_y;

    if (open_mode != MODE_HRV) {
        pixel_gsd_x = 1000 * poDS->msg_reader_core->get_col_dir_step();  // convert from km to m
        pixel_gsd_y = 1000 * poDS->msg_reader_core->get_line_dir_step(); // convert from km to m
        origin_x = -pixel_gsd_x * (-(Conversions::nlines / 2.0) + poDS->msg_reader_core->get_col_start());
        origin_y = -pixel_gsd_y * ((Conversions::nlines / 2.0) - poDS->msg_reader_core->get_line_start());
    } else {
        pixel_gsd_x = 1000 * poDS->msg_reader_core->get_col_dir_step() / 3.0;  // convert from km to m, approximate for HRV
        pixel_gsd_y = 1000 * poDS->msg_reader_core->get_line_dir_step() / 3.0; // convert from km to m, approximate for HRV
        origin_x = -pixel_gsd_x * (-(3*Conversions::nlines / 2.0) + 3*poDS->msg_reader_core->get_col_start());
        origin_y = -pixel_gsd_y * ((3*Conversions::nlines / 2.0) - 3*poDS->msg_reader_core->get_line_start());
    }

    poDS->adfGeoTransform[0] = origin_x;
    poDS->adfGeoTransform[1] = pixel_gsd_x;
    poDS->adfGeoTransform[2] = 0.0;

    poDS->adfGeoTransform[3] = origin_y;
    poDS->adfGeoTransform[4] = 0.0;
    poDS->adfGeoTransform[5] = -pixel_gsd_y;

    OGRSpatialReference oSRS;

    oSRS.SetProjCS("Geostationary projection (MSG)");
    oSRS.SetGEOS(  0, 35785831, 0, 0 );
    oSRS.SetGeogCS(
        "MSG Ellipsoid",
        "MSG_DATUM",
        "MSG_SPHEROID",
        Conversions::rpol * 1000.0,
        1 / ( 1 - Conversions::rpol/Conversions::req)
    );

    CPLFree(poDS->pszProjection);
    poDS->pszProjection = nullptr;
    oSRS.exportToWkt( &(poDS->pszProjection) );

    const CALIBRATION* cal = poDS->msg_reader_core->get_calibration_parameters();
    char tagname[30];
    char field[300];

    poDS->SetMetadataItem("Radiometric parameters format", "offset slope");
    for (i=1; i < band_count; i++) {
        snprintf(tagname, sizeof(tagname), "ch%02u_cal", band_map[i]);
        CPLsnprintf(field, sizeof(field), "%.12e %.12e", cal[band_map[i]-1].cal_offset, cal[band_map[i]-1].cal_slope);
        poDS->SetMetadataItem(tagname, field);
    }

    snprintf(field, sizeof(field), "%04u%02u%02u/%02u:%02u",
        poDS->msg_reader_core->get_year(),
        poDS->msg_reader_core->get_month(),
        poDS->msg_reader_core->get_day(),
        poDS->msg_reader_core->get_hour(),
        poDS->msg_reader_core->get_minute()
    );
    poDS->SetMetadataItem("Date/Time", field);

    snprintf(field, sizeof(field), "%u %u",
         poDS->msg_reader_core->get_line_start(),
         poDS->msg_reader_core->get_col_start()
    );
    poDS->SetMetadataItem("Origin", field);

    if (open_info != poOpenInfo) {
        delete open_info;
    }

    return poDS;
}

/************************************************************************/
/*                          GDALRegister_MSGN()                         */
/************************************************************************/

void GDALRegister_MSGN()

{
    if( GDALGetDriverByName( "MSGN" ) != nullptr )
        return;

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription( "MSGN" );
    poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
    poDriver->SetMetadataItem( GDAL_DMD_LONGNAME,
                               "EUMETSAT Archive native (.nat)" );
    poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "drivers/raster/msgn.html" );
    poDriver->SetMetadataItem( GDAL_DMD_EXTENSION, "nat" );
    poDriver->SetMetadataItem( GDAL_DCAP_VIRTUALIO, "YES" );

    poDriver->pfnOpen = MSGNDataset::Open;

    GetGDALDriverManager()->RegisterDriver( poDriver );
}
