/******************************************************************************
 *
 * Purpose:  Implementation of the Create() function to create new PCIDSK files.
 * 
 ******************************************************************************
 * Copyright (c) 2009
 * PCI Geomatics, 50 West Wilmot Street, Richmond Hill, Ont, Canada
 *
 * 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 "pcidsk.h"
#include "pcidsk_config.h"
#include "pcidsk_types.h"
#include "pcidsk_exception.h"
#include "pcidsk_file.h"
#include "pcidsk_georef.h"
#include "core/pcidsk_utils.h"
#include "segment/sysblockmap.h"
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <cstdio>

using namespace PCIDSK;

/************************************************************************/
/*                               Create()                               */
/************************************************************************/

/**
 * Create a PCIDSK (.pix) file. 
 *
 * @param filename the name of the PCIDSK file to create.
 * @param pixels the width of the new file in pixels.
 * @param lines the height of the new file in scanlines.
 * @param channel_count the number of channels to create.
 * @param channel_types an array of types for all the channels, or NULL for
 * all CHN_8U channels.
 * @param options creation options (interleaving, etc)
 * @param interfaces Either NULL to use default interfaces, or a pointer
 * to a populated interfaces object. 
 *
 * @return a pointer to a file object for accessing the PCIDSK file. 
 */

PCIDSKFile PCIDSK_DLL *
PCIDSK::Create( std::string filename, int pixels, int lines,
                int channel_count, eChanType *channel_types,
                std::string options, const PCIDSKInterfaces *interfaces )

{
    if( pixels < 0 || pixels > 99999999 ||
        lines < 0 || lines > 99999999 ||
        channel_count < 0 || channel_count > 99999999 )
    {
        return (PCIDSKFile*)ThrowPCIDSKExceptionPtr(
            "PCIDSK::Create(): invalid dimensions / band count." );
    }

/* -------------------------------------------------------------------- */
/*      Use default interfaces if none are passed in.                   */
/* -------------------------------------------------------------------- */
    PCIDSKInterfaces default_interfaces;
    if( interfaces == nullptr )
        interfaces = &default_interfaces;

/* -------------------------------------------------------------------- */
/*      Default the channel types to all 8U if not provided.            */
/* -------------------------------------------------------------------- */
    std::vector<eChanType> default_channel_types;

    if( channel_types == nullptr )
    {
        default_channel_types.resize( channel_count+1, CHN_8U );
        channel_types = &(default_channel_types[0]);
    }

/* -------------------------------------------------------------------- */
/*      Validate parameters.                                            */
/* -------------------------------------------------------------------- */
    const char *interleaving = nullptr;
    std::string compression = "NONE";
    /* bool nozero = false; */
    bool nocreate = false;
    bool externallink = false;
    int  blocksize = 127;

    UCaseStr( options );

    if(STARTS_WITH(options.c_str(), "PIXEL") )
        interleaving = "PIXEL";
    else if( STARTS_WITH(options.c_str(), "BAND") )
        interleaving = "BAND";
    else if( STARTS_WITH(options.c_str(), "TILED") )
    {
        interleaving = "FILE";
        ParseTileFormat( options, blocksize, compression );
    }
    else if( STARTS_WITH(options.c_str(), "FILE") )
    {
        if( STARTS_WITH(options.c_str(), "FILENOCREATE") )
            nocreate = true;
        else if( STARTS_WITH(options.c_str(), "FILELINK") )
        {
            nocreate = true;
            externallink = true;
        }
        interleaving = "FILE";
    }
    else
        return (PCIDSKFile*)ThrowPCIDSKExceptionPtr( "PCIDSK::Create() options '%s' not recognised.",
                              options.c_str() );
#if 0
    if( strstr(options.c_str(),"NOZERO") != NULL )
        nozero = true;
#endif

/* -------------------------------------------------------------------- */
/*      Validate the channel types.                                     */
/* -------------------------------------------------------------------- */
    int channels[7] = {0,0,0,0,0,0,0};
    int chan_index;
    bool regular = true;

    for( chan_index=0; chan_index < channel_count; chan_index++ )
    {
        if( chan_index > 0 
            && ((int) channel_types[chan_index]) 
                < ((int) channel_types[chan_index-1]) )
            regular = false;
        
        channels[((int) channel_types[chan_index])]++;
    }
    
    if( !regular && strcmp(interleaving,"FILE") != 0 )
    {
        return (PCIDSKFile*)ThrowPCIDSKExceptionPtr( 
           "Requested mixture of band types not supported for interleaving=%s.",
           interleaving );
    }
    
/* -------------------------------------------------------------------- */
/*      Create the file.                                                */
/* -------------------------------------------------------------------- */
    void *io_handle = interfaces->io->Open( filename, "w+" );

    assert( io_handle != nullptr );
    try
    {
/* ==================================================================== */
/*      Establish some key file layout information.                     */
/* ==================================================================== */
        int image_header_start = 1;                    // in blocks
        uint64 image_data_start, image_data_size=0;    // in blocks
        uint64 segment_ptr_start, segment_ptr_size=64; // in blocks
        int pixel_group_size, line_size;               // in bytes
        int image_header_count = channel_count;

/* -------------------------------------------------------------------- */
/*      Pixel interleaved.                                              */
/* -------------------------------------------------------------------- */
        if( strcmp(interleaving,"PIXEL") == 0 )
        {
            pixel_group_size = 
                channels[0] + // CHN_8U
                channels[1] * DataTypeSize(CHN_16U) + 
                channels[2] * DataTypeSize(CHN_16S) + 
                channels[3] * DataTypeSize(CHN_32R) +
                channels[4] * DataTypeSize(CHN_C16U) +
                channels[5] * DataTypeSize(CHN_C16S) +
                channels[6] * DataTypeSize(CHN_C32R);
                //channels[0] + channels[1]*2 + channels[2]*2 + channels[3]*4;
            line_size = ((pixel_group_size * pixels + 511) / 512) * 512;
            image_data_size = (((uint64)line_size) * lines) / 512;

            // TODO: Old code enforces a 1TB limit for some reason.
        }

/* -------------------------------------------------------------------- */
/*      Band interleaved.                                               */
/* -------------------------------------------------------------------- */
        else if( strcmp(interleaving,"BAND") == 0 )
        {
            pixel_group_size = 
                channels[0] + // CHN_8U
                channels[1] * DataTypeSize(CHN_16U) + 
                channels[2] * DataTypeSize(CHN_16S) + 
                channels[3] * DataTypeSize(CHN_32R) +
                channels[4] * DataTypeSize(CHN_C16U) +
                channels[5] * DataTypeSize(CHN_C16S) +
                channels[6] * DataTypeSize(CHN_C32R);
            // BAND interleaved bands are tightly packed.
            image_data_size = 
                (((uint64)pixel_group_size) * pixels * lines + 511) / 512;

            // TODO: Old code enforces a 1TB limit for some reason.
        }

/* -------------------------------------------------------------------- */
/*      FILE/Tiled.                                                     */
/* -------------------------------------------------------------------- */
        else if( strcmp(interleaving,"FILE") == 0 )
        {
            // For some reason we reserve extra space, but only for FILE.
            if( channel_count < 64 )
                image_header_count = 64;

            image_data_size = 0;

            // TODO: Old code enforces a 1TB limit on the fattest band.
        }

/* -------------------------------------------------------------------- */
/*      Place components.                                               */
/* -------------------------------------------------------------------- */
        segment_ptr_start = image_header_start + image_header_count*2;
        image_data_start = segment_ptr_start + segment_ptr_size;

/* ==================================================================== */
/*      Prepare the file header.                                        */
/* ==================================================================== */
        PCIDSKBuffer fh(512);

        char current_time[17];
        GetCurrentDateTime( current_time );

        // Initialize everything to spaces.
        fh.Put( "", 0, 512 );

/* -------------------------------------------------------------------- */
/*      File Type, Version, and Size                                    */
/*      Notice: we get the first 4 characters from PCIVERSIONAME.       */
/* -------------------------------------------------------------------- */
        // FH1 - magic format string.
        fh.Put( "PCIDSK", 0, 8 );

        // FH2 - TODO: Allow caller to pass this in.
        fh.Put( "SDK V1.0", 8, 8 );

        // FH3 - file size later.
        fh.Put( (image_data_start + image_data_size), 16, 16 );
        
        // FH4 - 16 characters reserved - spaces.

        // FH5 - Description
        fh.Put( filename.c_str(), 48, 64 );

        // FH6 - Facility
        fh.Put( "PCI Inc., Richmond Hill, Canada", 112, 32 );

        // FH7.1 / FH7.2 - left blank (64+64 bytes @ 144)

        // FH8 Creation date/time
        fh.Put( current_time, 272, 16 );

        // FH9 Update date/time
        fh.Put( current_time, 288, 16 );

/* -------------------------------------------------------------------- */
/*      Image Data                                                      */
/* -------------------------------------------------------------------- */
        // FH10 - start block of image data
        fh.Put( image_data_start+1, 304, 16 );

        // FH11 - number of blocks of image data.
        fh.Put( image_data_size, 320, 16 );

        // FH12 - start block of image headers.
        fh.Put( image_header_start+1, 336, 16 );

        // FH13 - number of blocks of image headers.
        fh.Put( image_header_count*2, 352, 8);

        // FH14 - interleaving.
        fh.Put( interleaving, 360, 8);

        // FH15 - reserved - MIXED is for some ancient backwards compatibility.
        fh.Put( "MIXED", 368, 8);

        // FH16 - number of image bands.
        fh.Put( channel_count, 376, 8 );

        // FH17 - width of image in pixels.
        fh.Put( pixels, 384, 8 );

        // FH18 - height of image in pixels.
        fh.Put( lines, 392, 8 );

        // FH19 - pixel ground size interpretation.
        fh.Put( "METRE", 400, 8 );

        // TODO:
        //PrintDouble( fh->XPixelSize, "%16.9f", 1.0 );
        //PrintDouble( fh->YPixelSize, "%16.9f", 1.0 );
        fh.Put( "1.0", 408, 16 );
        fh.Put( "1.0", 424, 16 );

/* -------------------------------------------------------------------- */
/*      Segment Pointers                                                */
/* -------------------------------------------------------------------- */
        // FH22 - start block of segment pointers.
        fh.Put( segment_ptr_start+1, 440, 16 );

        // fH23 - number of blocks of segment pointers.
        fh.Put( segment_ptr_size, 456, 8 );

/* -------------------------------------------------------------------- */
/*      Number of different types of Channels                           */
/* -------------------------------------------------------------------- */
        // FH24.1 - 8U bands.
        fh.Put( channels[0], 464, 4 );

        // FH24.2 - 16S bands.
        fh.Put( channels[1], 468, 4 );

        // FH24.3 - 16U bands.
        fh.Put( channels[2], 472, 4 );

        // FH24.4 - 32R bands.
        fh.Put( channels[3], 476, 4 );
        
        // FH24.5 - C16U bands
        fh.Put( channels[4], 480, 4 );
        
        // FH24.6 - C16S bands
        fh.Put( channels[5], 484, 4 );
        
        // FH24.7 - C32R bands
        fh.Put( channels[6], 488, 4 );

/* -------------------------------------------------------------------- */
/*      Write out the file header.                                      */
/* -------------------------------------------------------------------- */
        interfaces->io->Write( fh.buffer, 512, 1, io_handle );

/* ==================================================================== */
/*      Write out the image headers.                                    */
/* ==================================================================== */
        PCIDSKBuffer ih( 1024 );

        ih.Put( " ", 0, 1024 );

        // IHi.1 - Text describing Channel Contents
        ih.Put( "Contents Not Specified", 0, 64 );

        // IHi.2 - Filename storing image.
        if( STARTS_WITH(interleaving, "FILE") )
            ih.Put( "<unintialized>", 64, 64 );  // TODO: Spelling?
        
        if( externallink )
        {
            // IHi.6.7 - IHi.6.10
            ih.Put( 0, 250, 8 ); 
            ih.Put( 0, 258, 8 );
            ih.Put( pixels, 266, 8 );
            ih.Put( lines, 274, 8 );
        }

        // IHi.3 - Creation time and date.
        ih.Put( current_time, 128, 16 );

        // IHi.4 - Creation time and date.
        ih.Put( current_time, 144, 16 );

        interfaces->io->Seek( io_handle, image_header_start*512, SEEK_SET );

        for( chan_index = 0; chan_index < channel_count; chan_index++ )
        {
            ih.Put(DataTypeName(channel_types[chan_index]).c_str(), 160, 8);    

            if( STARTS_WITH(options.c_str(), "TILED") )
            {
                char sis_filename[65];
                snprintf( sis_filename, sizeof(sis_filename), "/SIS=%d", chan_index );
                ih.Put( sis_filename, 64, 64 );

                // IHi.6.7 - IHi.6.10
                ih.Put( 0, 250, 8 ); 
                ih.Put( 0, 258, 8 );
                ih.Put( pixels, 266, 8 );
                ih.Put( lines, 274, 8 );

                // IHi.6.11
                ih.Put( 1, 282, 8 );
            }

            interfaces->io->Write( ih.buffer, 1024, 1, io_handle );
        }

        for( chan_index = channel_count; 
            chan_index < image_header_count; 
            chan_index++ )
        {
            ih.Put( "", 160, 8 );
            ih.Put( "<unintialized>", 64, 64 );  // TODO: Spelling?
            ih.Put( "", 250, 40 );

            interfaces->io->Write( ih.buffer, 1024, 1, io_handle );
        }

/* ==================================================================== */
/*      Write out the segment pointers, all spaces.                     */
/* ==================================================================== */
        PCIDSKBuffer segment_pointers( (int) (segment_ptr_size*512) );
        segment_pointers.Put( " ", 0, (int) (segment_ptr_size*512) );

        interfaces->io->Seek( io_handle, segment_ptr_start*512, SEEK_SET );
        interfaces->io->Write( segment_pointers.buffer, segment_ptr_size, 512, 
                            io_handle );

/* -------------------------------------------------------------------- */
/*      Ensure we write out something at the end of the image data      */
/*      to force the file size.                                         */
/* -------------------------------------------------------------------- */
        if( image_data_size > 0 )
        {
            interfaces->io->Seek( io_handle, (image_data_start + image_data_size)*512-1,
                                SEEK_SET );
            interfaces->io->Write( "\0", 1, 1, io_handle );
        }
    
/* -------------------------------------------------------------------- */
/*      Close the raw file, and reopen as a pcidsk file.                */
/* -------------------------------------------------------------------- */
        interfaces->io->Close( io_handle );
    }
    catch( const PCIDSKException& )
    {
        interfaces->io->Close( io_handle );
        throw;
    }

    PCIDSKFile *file = Open( filename, "r+", interfaces );
    try
    {
/* -------------------------------------------------------------------- */
/*      Create a default georeferencing segment.                        */
/* -------------------------------------------------------------------- */
        file->CreateSegment( "GEOref", 
                            "Master Georeferencing Segment for File",
                            SEG_GEO, 6 );

/* -------------------------------------------------------------------- */
/*      If the dataset is tiled, create the file band data.             */
/* -------------------------------------------------------------------- */
        if( STARTS_WITH(options.c_str(), "TILED") )
        {
            file->SetMetadataValue( "_DBLayout", options ); 

            // For sizing the SysBMDir we want an approximate size of the
            // the imagery.
            uint64 rough_image_size = 
                (channels[0] + // CHN_8U
                channels[1] * DataTypeSize(CHN_16U) + 
                channels[2] * DataTypeSize(CHN_16S) + 
                channels[3] * DataTypeSize(CHN_32R) +
                channels[4] * DataTypeSize(CHN_C16U) +
                channels[5] * DataTypeSize(CHN_C16S) +
                channels[6] * DataTypeSize(CHN_C32R)) 
                * (pixels * (uint64) lines);
            uint64 sysbmdir_size = ((rough_image_size / 8192) * 28) / 512;

            sysbmdir_size = (int) (sysbmdir_size * 1.1 + 100);
            int segment = file->CreateSegment( "SysBMDir", 
                                            "System Block Map Directory - Do not modify.",
                                            SEG_SYS, static_cast<int>(sysbmdir_size) );
            
            SysBlockMap *bm = 
                dynamic_cast<SysBlockMap *>(file->GetSegment( segment ));

            for( chan_index = 0; chan_index < channel_count; chan_index++ )
            {
                bm->CreateVirtualImageFile( pixels, lines, blocksize, blocksize,
                                            channel_types[chan_index], 
                                            compression );
            }
        }

/* -------------------------------------------------------------------- */
/*      If we have a non-tiled FILE interleaved file, should we         */
/*      create external band files now?                                 */
/* -------------------------------------------------------------------- */
        if( STARTS_WITH(interleaving, "FILE") 
            && !STARTS_WITH(options.c_str(), "TILED") 
            && !nocreate )
        {
            for( chan_index = 0; chan_index < channel_count; chan_index++ )
            {
                PCIDSKChannel *channel = file->GetChannel( chan_index + 1 );
                int pixel_size = DataTypeSize(channel->GetType());

                // build a band filename that uses the basename of the PCIDSK
                // file, and adds ".nnn" based on the band. 
                std::string band_filename = filename;
                char ext[5];
                CPLsnprintf( ext, sizeof(ext), ".%03d", chan_index+1 );

                size_t last_dot = band_filename.find_last_of(".");
                if( last_dot != std::string::npos 
                    && (band_filename.find_last_of("/\\:") == std::string::npos
                        || band_filename.find_last_of("/\\:") < last_dot) )
                {
                    band_filename.resize( last_dot );
                }

                band_filename += ext;

                // Now build a version without a path. 
                std::string relative_band_filename;
                size_t path_div = band_filename.find_last_of( "/\\:" );
                if( path_div == std::string::npos )
                    relative_band_filename = band_filename;
                else
                    relative_band_filename = band_filename.c_str() + path_div + 1;
                
                // create the file - ought we write the whole file?
                void *band_io_handle = interfaces->io->Open( band_filename, "w" );
                interfaces->io->Write( "\0", 1, 1, band_io_handle );
                interfaces->io->Close( band_io_handle );

                // Set the channel header information.
                channel->SetChanInfo( relative_band_filename, 0, pixel_size, 
                                    pixel_size * pixels, true );
            }
        }
    }
    catch( const PCIDSKException& )
    {
        delete file;
        throw;
    }

    return file;
}
