/******************************************************************************
 *
 * Project:  USGS DOQ Driver (Second Generation Format)
 * Purpose:  Implementation of DOQ2Dataset
 * Author:   Derrick J Brashear, shadow@dementia.org
 *
 ******************************************************************************
 * Copyright (c) 2000, Derrick J Brashear
 * Copyright (c) 2009-2011, Even Rouault <even dot rouault at mines-paris dot org>
 *
 * 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 "cpl_string.h"
#include "gdal_frmts.h"
#include "rawdataset.h"

CPL_CVSID("$Id: doq2dataset.cpp 36501 2016-11-25 14:09:24Z rouault $");

static const char UTM_FORMAT[] =
    "PROJCS[\"%s / UTM zone %dN\",GEOGCS[%s,PRIMEM[\"Greenwich\",0],"
    "UNIT[\"degree\",0.0174532925199433]],PROJECTION[\"Transverse_Mercator\"],"
    "PARAMETER[\"latitude_of_origin\",0],PARAMETER[\"central_meridian\",%d],"
    "PARAMETER[\"scale_factor\",0.9996],PARAMETER[\"false_easting\",500000],"
    "PARAMETER[\"false_northing\",0],%s]";

static const char WGS84_DATUM[] =
    "\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563]]";

static const char WGS72_DATUM[] =
    "\"WGS 72\",DATUM[\"WGS_1972\",SPHEROID[\"NWL 10D\",6378135,298.26]]";

static const char NAD27_DATUM[] =
    "\"NAD27\",DATUM[\"North_American_Datum_1927\","
    "SPHEROID[\"Clarke 1866\",6378206.4,294.978698213901]]";

static const char NAD83_DATUM[] =
    "\"NAD83\",DATUM[\"North_American_Datum_1983\","
    "SPHEROID[\"GRS 1980\",6378137,298.257222101]]";

/************************************************************************/
/* ==================================================================== */
/*                              DOQ2Dataset                             */
/* ==================================================================== */
/************************************************************************/

class DOQ2Dataset : public RawDataset
{
    VSILFILE    *fpImage;  // Image data file.

    double      dfULX;
    double      dfULY;
    double      dfXPixelSize;
    double      dfYPixelSize;

    char        *pszProjection;

  public:
                DOQ2Dataset();
                ~DOQ2Dataset();

    CPLErr      GetGeoTransform( double * padfTransform ) override;
    const char  *GetProjectionRef( void ) override;

    static GDALDataset *Open( GDALOpenInfo * );
};

/************************************************************************/
/*                            DOQ2Dataset()                             */
/************************************************************************/

DOQ2Dataset::DOQ2Dataset() :
    fpImage(NULL),
    dfULX(0.0),
    dfULY(0.0),
    dfXPixelSize(0.0),
    dfYPixelSize(0.0),
    pszProjection(NULL)
{ }

/************************************************************************/
/*                            ~DOQ2Dataset()                            */
/************************************************************************/

DOQ2Dataset::~DOQ2Dataset()

{
    FlushCache();

    CPLFree( pszProjection );
    if( fpImage != NULL )
        CPL_IGNORE_RET_VAL(VSIFCloseL( fpImage ));
}

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

CPLErr DOQ2Dataset::GetGeoTransform( double * padfTransform )

{
    padfTransform[0] = dfULX;
    padfTransform[1] = dfXPixelSize;
    padfTransform[2] = 0.0;
    padfTransform[3] = dfULY;
    padfTransform[4] = 0.0;
    padfTransform[5] = -1 * dfYPixelSize;

    return CE_None;
}

/************************************************************************/
/*                        GetProjectionString()                         */
/************************************************************************/

const char *DOQ2Dataset::GetProjectionRef()

{
    return pszProjection;
}

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

GDALDataset *DOQ2Dataset::Open( GDALOpenInfo * poOpenInfo )

{
/* -------------------------------------------------------------------- */
/*      We assume the user is pointing to the binary (i.e. .bil) file.  */
/* -------------------------------------------------------------------- */
    if( poOpenInfo->nHeaderBytes < 212 )
        return NULL;

    if(! STARTS_WITH_CI( reinterpret_cast<char *>( poOpenInfo->pabyHeader ),
                         "BEGIN_USGS_DOQ_HEADER" ) )
        return NULL;

    VSILFILE* fp = VSIFOpenL(poOpenInfo->pszFilename, "rb");
    if (fp == NULL)
        return NULL;

    int nLineCount = 0;
    int nBytesPerPixel = 0;
    int nWidth = 0;
    int nHeight = 0;
    int nBandStorage = 0;
    int nBandTypes = 0;
    const char *pszDatumLong = NULL;
    const char *pszDatumShort = NULL;
    const char *pszUnits = NULL;
    char *pszQuadname = NULL;
    char *pszQuadquad = NULL;
    char *pszState = NULL;
    int nZone = 0;
    int nProjType = 0;
    int nSkipBytes = 0;
    int nBandCount = 0;
    double dfULXMap=0.0;
    double dfULYMap = 0.0;
    double dfXDim = 0.0;
    double dfYDim = 0.0;
    char **papszMetadata = NULL;

    /* read and discard the first line */
    CPL_IGNORE_RET_VAL(CPLReadLineL( fp ));

    const char *pszLine = NULL;
    while( (pszLine = CPLReadLineL( fp )) != NULL )
    {
        nLineCount++;

        if( EQUAL(pszLine,"END_USGS_DOQ_HEADER") )
            break;

        char **papszTokens = CSLTokenizeString( pszLine );
        if( CSLCount( papszTokens ) < 2 )
        {
            CSLDestroy( papszTokens );
            break;
        }

        if( EQUAL( papszTokens[0], "SAMPLES_AND_LINES" ) &&
            CSLCount( papszTokens ) >= 3 )
        {
            nWidth = atoi(papszTokens[1]);
            nHeight = atoi(papszTokens[2]);
        }
        else if( EQUAL(papszTokens[0],"BYTE_COUNT") )
        {
            nSkipBytes = atoi(papszTokens[1]);
        }
        else if( EQUAL( papszTokens[0], "XY_ORIGIN" ) &&
                 CSLCount( papszTokens ) >= 3 )
        {
            dfULXMap = CPLAtof(papszTokens[1]);
            dfULYMap = CPLAtof(papszTokens[2]);
        }
        else if( EQUAL( papszTokens[0], "HORIZONTAL_RESOLUTION" ) )
        {
            dfXDim = CPLAtof(papszTokens[1]);
            dfYDim = dfXDim;
        }
        else if( EQUAL(papszTokens[0], "BAND_ORGANIZATION") )
        {
            if( EQUAL(papszTokens[1], "SINGLE FILE") )
                nBandStorage = 1;
            if( EQUAL(papszTokens[1], "BSQ") )
                nBandStorage = 1;
            if( EQUAL(papszTokens[1], "BIL") )
                nBandStorage = 1;
            if( EQUAL(papszTokens[1], "BIP") )
                nBandStorage = 4;
        }
        else if( EQUAL(papszTokens[0], "BAND_CONTENT") )
        {
            if( EQUAL(papszTokens[1], "BLACK&WHITE") )
                nBandTypes = 1;
            else if( EQUAL(papszTokens[1], "COLOR") )
                nBandTypes = 5;
            else if( EQUAL(papszTokens[1], "RGB") )
                nBandTypes = 5;
            else if( EQUAL(papszTokens[1], "RED") )
                nBandTypes = 5;
            else if( EQUAL(papszTokens[1], "GREEN") )
                nBandTypes = 5;
            else if( EQUAL(papszTokens[1], "BLUE") )
                nBandTypes = 5;

            nBandCount++;
        }
        else if( EQUAL(papszTokens[0], "BITS_PER_PIXEL") )
        {
            nBytesPerPixel = atoi(papszTokens[1]) / 8;
        }
        else if( EQUAL(papszTokens[0], "HORIZONTAL_COORDINATE_SYSTEM") )
        {
            if( EQUAL(papszTokens[1], "UTM") )
                nProjType = 1;
            else if( EQUAL(papszTokens[1], "SPCS") )
                nProjType = 2;
            else if( EQUAL(papszTokens[1], "GEOGRAPHIC") )
                nProjType = 0;
        }
        else if( EQUAL(papszTokens[0], "COORDINATE_ZONE") )
        {
            nZone = atoi(papszTokens[1]);
        }
        else if( EQUAL(papszTokens[0], "HORIZONTAL_UNITS") )
        {
            if( EQUAL(papszTokens[1], "METERS") )
                pszUnits = "UNIT[\"metre\",1]";
            else if( EQUAL(papszTokens[1], "FEET") )
                pszUnits = "UNIT[\"US survey foot\",0.304800609601219]";
        }
        else if( EQUAL(papszTokens[0],"HORIZONTAL_DATUM") )
        {
            if( EQUAL(papszTokens[1], "NAD27") )
            {
                pszDatumLong = NAD27_DATUM;
                pszDatumShort = "NAD 27";
            }
            else if( EQUAL(papszTokens[1]," WGS72") )
            {
                pszDatumLong = WGS72_DATUM;
                pszDatumShort = "WGS 72";
            }
            else if( EQUAL(papszTokens[1], "WGS84") )
            {
                pszDatumLong = WGS84_DATUM;
                pszDatumShort = "WGS 84";
            }
            else if( EQUAL(papszTokens[1], "NAD83") )
            {
                pszDatumLong = NAD83_DATUM;
                pszDatumShort = "NAD 83";
            }
            else
            {
                pszDatumLong = "DATUM[\"unknown\"]";
                pszDatumShort = "unknown";
            }
        }
        else
        {
            /* we want to generically capture all the other metadata */
            CPLString osMetaDataValue;

            for( int iToken = 1; papszTokens[iToken] != NULL; iToken++ )
            {
                if( EQUAL(papszTokens[iToken],"*") )
                    continue;

                if( iToken > 1 )
                    osMetaDataValue += " " ;
                osMetaDataValue += papszTokens[iToken];
            }
            papszMetadata = CSLAddNameValue( papszMetadata,
                                             papszTokens[0],
                                             osMetaDataValue );
        }

        CSLDestroy( papszTokens );
    }

    CPLReadLineL( NULL );

/* -------------------------------------------------------------------- */
/*      Do these values look coherent for a DOQ file?  It would be      */
/*      nice to do a more comprehensive test than this!                 */
/* -------------------------------------------------------------------- */
    if( nWidth < 500 || nWidth > 25000
        || nHeight < 500 || nHeight > 25000
        || nBandStorage < 0 || nBandStorage > 4
        || nBandTypes < 1 || nBandTypes > 9 )
    {
        CSLDestroy( papszMetadata );
        CPL_IGNORE_RET_VAL(VSIFCloseL(fp));
        return NULL;
    }

/* -------------------------------------------------------------------- */
/*      Check the configuration.  We don't currently handle all         */
/*      variations, only the common ones.                               */
/* -------------------------------------------------------------------- */
    if( nBandTypes > 5 )
    {
        CPLError( CE_Failure, CPLE_OpenFailed,
                  "DOQ Data Type (%d) is not a supported configuration.",
                  nBandTypes );
        CSLDestroy( papszMetadata );
        CPL_IGNORE_RET_VAL(VSIFCloseL(fp));
        return NULL;
    }

/* -------------------------------------------------------------------- */
/*      Confirm the requested access is supported.                      */
/* -------------------------------------------------------------------- */
    if( poOpenInfo->eAccess == GA_Update )
    {
        CSLDestroy( papszMetadata );
        CPLError( CE_Failure, CPLE_NotSupported,
                  "The DOQ2 driver does not support update access to existing"
                  " datasets." );
        CPL_IGNORE_RET_VAL(VSIFCloseL(fp));
        return NULL;
    }
/* -------------------------------------------------------------------- */
/*      Create a corresponding GDALDataset.                             */
/* -------------------------------------------------------------------- */
    DOQ2Dataset *poDS = new DOQ2Dataset();

    poDS->nRasterXSize = nWidth;
    poDS->nRasterYSize = nHeight;

    poDS->SetMetadata( papszMetadata );
    CSLDestroy( papszMetadata );
    papszMetadata = NULL;

    poDS->fpImage = fp;

/* -------------------------------------------------------------------- */
/*      Compute layout of data.                                         */
/* -------------------------------------------------------------------- */
    if( nBandCount < 2 )
        nBandCount = nBytesPerPixel;
    else
        nBytesPerPixel *= nBandCount;

    const int nBytesPerLine = nBytesPerPixel * nWidth;

/* -------------------------------------------------------------------- */
/*      Create band information objects.                                */
/* -------------------------------------------------------------------- */
    CPLErrorReset();
    for( int i = 0; i < nBandCount; i++ )
    {
        poDS->SetBand( i+1,
            new RawRasterBand( poDS, i+1, poDS->fpImage,
                               nSkipBytes + i, nBytesPerPixel, nBytesPerLine,
                               GDT_Byte, TRUE, TRUE ) );
        if( CPLGetLastErrorType() != CE_None )
        {
            delete poDS;
            CPLFree( pszQuadname );
            CPLFree( pszQuadquad );
            CPLFree( pszState );
            return NULL;
        }
    }

    if (nProjType == 1)
    {
        poDS->pszProjection =
            CPLStrdup( CPLSPrintf(
                UTM_FORMAT, pszDatumShort ? pszDatumShort : "", nZone,
                pszDatumLong ? pszDatumLong : "",
                nZone >= 1 && nZone <= 60 ? nZone * 6 - 183 : 0,
                pszUnits ? pszUnits : "" ) );
    }
    else
    {
        poDS->pszProjection = CPLStrdup("");
    }

    poDS->dfULX = dfULXMap;
    poDS->dfULY = dfULYMap;

    poDS->dfXPixelSize = dfXDim;
    poDS->dfYPixelSize = dfYDim;

    CPLFree( pszQuadname );
    CPLFree( pszQuadquad );
    CPLFree( pszState );

/* -------------------------------------------------------------------- */
/*      Initialize any PAM information.                                 */
/* -------------------------------------------------------------------- */
    poDS->SetDescription( poOpenInfo->pszFilename );
    poDS->TryLoadXML();

/* -------------------------------------------------------------------- */
/*      Check for overviews.                                            */
/* -------------------------------------------------------------------- */
    poDS->oOvManager.Initialize( poDS, poOpenInfo->pszFilename );

    return poDS;
}

/************************************************************************/
/*                         GDALRegister_DOQ1()                          */
/************************************************************************/

void GDALRegister_DOQ2()

{
    if( GDALGetDriverByName( "DOQ2" ) != NULL )
        return;

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription( "DOQ2" );
    poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
    poDriver->SetMetadataItem( GDAL_DMD_LONGNAME, "USGS DOQ (New Style)" );
    poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "frmt_various.html#DOQ2" );
    poDriver->SetMetadataItem( GDAL_DCAP_VIRTUALIO, "YES" );

    poDriver->pfnOpen = DOQ2Dataset::Open;

    GetGDALDriverManager()->RegisterDriver( poDriver );
}
