/******************************************************************************
 *
 * Project:  OpenCPN
 * Authors:  David Register
 *           Sean D'Epagnier
 *
 ***************************************************************************
 *   Copyright (C) 2014 by David S. Register                               *
 *                                                                         *
 *   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 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,  USA.         *
 ***************************************************************************
 */

#include <wx/wxprec.h>
#include <wx/tokenzr.h>
#include <wx/filename.h>
#include <wx/wx.h>

#include <stdint.h>

#include "config.h"

#include "dychart.h"

#include "viewport.h"
#include "glTexCache.h"
#include "glTextureDescriptor.h"

#include "chcanv.h"
#include "glChartCanvas.h"
#include "Quilt.h"
#include "chartbase.h"
#include "chartimg.h"
#include "chartdb.h"
#include "OCPNPlatform.h"
#include "mipmap/mipmap.h"

#ifndef GL_ETC1_RGB8_OES
#define GL_ETC1_RGB8_OES                                        0x8D64
#endif

#include "squish.h"
#include "lz4.h"
#include "lz4hc.h"

extern long g_tex_mem_used;
extern int g_mipmap_max_level;
extern GLuint g_raster_format;
extern int          g_memCacheLimit;

extern ColorScheme global_color_scheme;

extern ChartDB      *ChartData;
extern ocpnGLOptions    g_GLOptions;

extern int              g_tile_size;

extern PFNGLCOMPRESSEDTEXIMAGE2DPROC s_glCompressedTexImage2D;
extern PFNGLGENERATEMIPMAPEXTPROC          s_glGenerateMipmap;
extern bool GetMemoryStatus( int *mem_total, int *mem_used );

extern wxString CompressedCachePath(wxString path);
extern glTextureManager   *g_glTextureManager;

//      CatalogEntry implementation
CatalogEntry::CatalogEntry()
{
}

CatalogEntry::~CatalogEntry()
{
}

CatalogEntry::CatalogEntry(int level, int x0, int y0, ColorScheme colorscheme)
{
    k.mip_level = level;
    k.x = x0;
    k.y = y0;
    k.tcolorscheme = colorscheme;
}

int CatalogEntry::GetSerialSize()
{
    return CATALOG_ENTRY_SERIAL_SIZE;
}

void CatalogEntry::Serialize( unsigned char *t)
{
    uint32_t *p = (uint32_t *)t;
    
    *p++ = k.mip_level;
    *p++ = k.x;
    *p++ = k.y;
    *p++ = k.tcolorscheme;
    *p++ = v.texture_offset;
    *p++ = v.compressed_size;
    
}


void CatalogEntry::DeSerialize( unsigned char *t)
{
    uint32_t *p = (uint32_t *)t;
    
     k.mip_level = *p++;
     k.x = *p++;
     k.y = *p++;
     k.tcolorscheme = (ColorScheme)*p++;
     v.texture_offset = *p++;
     v.compressed_size = *p++;
     
}

//      glTexFactory Implementation
enum TextureDataType {COMPRESSED_BUFFER_OK, MAP_BUFFER_OK};

glTexFactory::glTexFactory(ChartBase *chart, int raster_format)
{
//    m_pchart = chart;
    n_catalog_entries = 0;
    m_catalog_offset = sizeof(CompressedCacheHeader);
    wxDateTime ed = chart->GetEditionDate();
    m_chart_date_binary = (uint32_t)ed.GetTicks();
    m_chartfile_date_binary = ::wxFileModificationTime(chart->GetFullPath());
    m_chartfile_size = (uint32_t)wxFileName::GetSize(chart->GetFullPath()).GetLo();
    m_ChartPath = chart->GetFullPath();
    
    m_CompressedCacheFilePath = CompressedCachePath(chart->GetFullPath());
    m_hdrOK = false;
    m_catalogOK = false;
    m_newCatalog = true;

    m_catalogCorrupted = false;

    m_fs = 0;
    m_LRUtime = 0;
    m_ntex = 0;
    m_tiles = NULL;
    for (int i = 0; i < N_COLOR_SCHEMES; i++) {
        for (int j = 0; j < MAX_TEX_LEVEL; j++) {
            m_cache[i][j] = NULL;
        }
    }
    //  Initialize the TextureDescriptor array
    ChartBaseBSB *pBSBChart = dynamic_cast<ChartBaseBSB*>( chart );
    
    if( !pBSBChart )
        return;
    
    m_size_X = pBSBChart->GetSize_X();
    m_size_Y = pBSBChart->GetSize_Y();
        
    //  Calculate the number of textures needed
    m_tex_dim = g_GLOptions.m_iTextureDimension;
    m_nx_tex = ( m_size_X / m_tex_dim ) + ((m_size_X % m_tex_dim) == 0 ? 0:1);
    m_ny_tex = ( m_size_Y / m_tex_dim ) + ((m_size_Y % m_tex_dim) == 0 ? 0:1);
    
    m_stride = m_nx_tex;
    m_ntex = m_nx_tex * m_ny_tex;
    m_td_array = (glTextureDescriptor **)calloc(m_ntex, sizeof(glTextureDescriptor *));

    m_prepared_projection_type = 0;
}

glTexFactory::~glTexFactory()
{
    delete m_fs;

    PurgeBackgroundCompressionPool();
    DeleteAllTextures();
    DeleteAllDescriptors();

    for (int i = 0; i < N_COLOR_SCHEMES; i++) {
        for (int j = 0; j < MAX_TEX_LEVEL; j++) {
            CatalogEntryValue *v = m_cache[i][j];
            if (v) {
                free(v);
            }
        }
    }
 
    free( m_td_array );         // array is empty

    if(m_tiles)
        for(int i=0 ; i < m_ntex; i++)
            delete m_tiles[i];
    delete [] m_tiles;
 }

glTextureDescriptor *glTexFactory::GetpTD( wxRect & rect )
{
    int array_index = ArrayIndex(rect.x, rect.y);
    return m_td_array[array_index];
}

bool glTexFactory::OnTimer()
{
    for(int i=0 ; i < m_ntex ; i++) {
        glTextureDescriptor *ptd = m_td_array[i] ;
        // sometimes compressed data is produced but by the time
        // it arrives it is no longer needed, so with a timeout
        // of 5 seconds free this memory to avoid ram use buildup
        if(ptd && ptd->compdata_ticks) {
            ptd->compdata_ticks--;
            ptd->FreeComp();
        }
    }

#if 0 // this is proven unreliable and slow
    // if we have the data in the catalog of level 0 or doubly compressed
    // for an entire row of tiles, then we can free rows from the linebuffer
    if(g_GLOptions.m_bTextureCompression) {
        ChartBase *pChart = ChartData->OpenChartFromDB( m_ChartPath, FULL_INIT );
        ChartBaseBSB *pBSBChart = dynamic_cast<ChartBaseBSB*>( pChart );

        if(pBSBChart) {
            for(int y = 0; y<m_ny_tex; y++) {
                int dim = g_GLOptions.m_iTextureDimension;

                if(!pBSBChart->HaveLineCacheRow(y*dim))
                    continue;

                for(int x = 0; x<m_nx_tex; x++) {
                    int i = ArrayIndex(x, y);
                    glTextureDescriptor *ptd = m_td_array[i];
                    
                    if( !ptd )
                        goto keeplines;
                    
                    if( ptd->compcomp_array[0] )
                        continue; // ok
                    
                    CatalogEntryValue *p = GetCacheEntryValue(0, x*dim, y*dim, ptd->m_colorscheme);
                    if(!p)
                        goto keeplines;
                }

                pBSBChart->FreeLineCacheRows(y*dim, (y+1)*dim);
            }
        keeplines:;
        }
    }
#endif
    
    // write doubly compressed data to disk
    if(g_GLOptions.m_bTextureCompressionCaching)
        for(int i=0 ; i < m_ntex ; i++) {
            glTextureDescriptor *ptd = m_td_array[i];
            if(ptd && ptd->IsCompCompArrayComplete( 0 )) {
                int dim = g_GLOptions.m_iTextureDimension;
                UpdateCacheAllLevels
                    (wxRect(ptd->x, ptd->y, dim, dim),
                     ptd->m_colorscheme, ptd->compcomp_array, ptd->compcomp_size );

                // no longer need to store the compressed compressed data
                ptd->FreeCompComp();
//                return true;
            }
        }

    return false;
}

#if 0
#ifdef __OCPN__ANDROID__
    // delete any uncompressed textures if texture memory is more than 30
    // on android??   Maybe this should be removed now
    bool bGLMemCrunch = g_tex_mem_used > 30/*g_GLOptions.m_iTextureMemorySize*/ * 1024 * 1024;
    
    if( bGLMemCrunch ){
        for(wxTextureListNode *node = m_texture_list.GetFirst(); node;
            node = node->GetNext()) {
            glTextureDescriptor *ptd = node->GetData();
            if(ptd->nGPU_compressed == GPU_TEXTURE_UNCOMPRESSED){
                DeleteSingleTexture(ptd);
            }
        }
    }
#endif    
#endif

void glTexFactory::AccumulateMemStatistics(int &map_size, int &comp_size, int &compcomp_size)
{
    for(int i=0 ; i < m_ntex ; i++) {
        glTextureDescriptor *ptd = m_td_array[i] ;
        if(ptd) {
            map_size += ptd->GetMapArrayAlloc();
            comp_size += ptd->GetCompArrayAlloc();
            compcomp_size += ptd->GetCompCompArrayAlloc();
        }
    }
}

void glTexFactory::DeleteTexture(const wxRect &rect)
{
    //    Is this texture tile defined?
    int array_index = ArrayIndex(rect.x, rect.y);
    glTextureDescriptor *ptd = m_td_array[array_index];

    
    if( ptd && ptd->tex_name > 0 ) {
        DeleteSingleTexture( ptd );
    }
}

void glTexFactory::DeleteAllTextures( void )
{
    // iterate over all the textures presently loaded
    // and delete the OpenGL texture from the GPU
    // but keep the private texture descriptor for now
  
    for(int i=0 ; i < m_ntex ; i++){
        glTextureDescriptor *ptd = m_td_array[i] ;
        
        if( ptd ) {
//            if(ptd->tex_name && bthread_debug)
//                printf("DAT::Delete Texture %d   resulting g_tex_mem_used, mb:  %ld\n", ptd->tex_name, g_tex_mem_used/(1024 * 1024));
            
            DeleteSingleTexture( ptd);
        }
    }
}


void glTexFactory::DeleteSomeTextures( long target )
{
    // iterate over all the textures presently loaded
    // and delete the OpenGL texture from the GPU
    // until the target g_tex_mem_used is reached
    // but keep the private texture descriptor for now
    
    for(int i=0 ; i < m_ntex ; i++){
        glTextureDescriptor *ptd = m_td_array[i] ;
        
        if( ptd ) {
//            if(ptd->tex_name && bthread_debug)
//                printf("DSoT::Delete Some Texture %d   resulting g_tex_mem_used, mb:  %ld\n", ptd->tex_name, g_tex_mem_used/(1024 * 1024));
            
            if(ptd->tex_name)
                DeleteSingleTexture( ptd);
        
            if(g_tex_mem_used <= target)
                break;
        }
    }
}

void glTexFactory::FreeSome( long target )
{
    for(int i=0 ; i < m_ntex ; i++){
        glTextureDescriptor *ptd = m_td_array[i] ;
        
        if( ptd )
            ptd->FreeMap();
    }
}


void glTexFactory::DeleteAllDescriptors( void )
{
    // iterate over all the texture descriptors

    for(int i=0 ; i < m_ntex ; i++){
        glTextureDescriptor *ptd = m_td_array[i] ;
        delete ptd;
        m_td_array[i] = 0;
    }
    
}

bool glTexFactory::BackgroundCompressionAsJob() const
{
    return g_glTextureManager->AsJob( m_ChartPath );
}

void glTexFactory::PurgeBackgroundCompressionPool()
{
    //  Purge the "todo" list, and allow any running jobs to complete normally
    g_glTextureManager->PurgeJobList( m_ChartPath );
}
        
void glTexFactory::DeleteSingleTexture( glTextureDescriptor *ptd )
{
    if(!ptd->tex_name)
        return;

    g_tex_mem_used -= ptd->tex_mem_used;
    ptd->level_min = g_mipmap_max_level + 1;  // default, nothing loaded

    glDeleteTextures( 1, &ptd->tex_name );
    ptd->tex_name = 0;
    ptd->tex_mem_used = 0;
    ptd->nGPU_compressed = GPU_TEXTURE_UNKNOWN;
}

void  glTexFactory::ArrayXY(wxRect *r, int index) const
{
    r->y = (index / m_stride)*m_tex_dim;
    r->x = (index -((r->y/m_tex_dim)*m_stride)) *m_tex_dim;
}

CatalogEntryValue *glTexFactory::GetCacheEntryValue(int level, int x, int y, ColorScheme color_scheme)
{
    if (level < 0 || level >= MAX_TEX_LEVEL)
        return 0;

    //  Look in the cache
    LoadCatalog();

    CatalogEntryValue *v = m_cache[color_scheme][level];
    if (v == 0)
        return 0;

    int array_index = ArrayIndex(x, y);
    if (array_index >= m_ntex)
        return 0;

    CatalogEntryValue *r = &v[array_index];
    if (r->compressed_size == 0)
        return 0;

    return r;    
}

bool glTexFactory::IsLevelInCache( int level, const wxRect &rect, ColorScheme color_scheme )
{
    bool b_ret = false;
    
    if(g_GLOptions.m_bTextureCompression &&
        g_GLOptions.m_bTextureCompressionCaching) {
         
    //  Search for the requested texture
        if (GetCacheEntryValue(level, rect.x, rect.y, color_scheme) != 0)
            b_ret = true;
    }
    
    return b_ret;
}

glTextureDescriptor *glTexFactory::GetOrCreateTD(const wxRect &rect)
{
    int array_index = ArrayIndex(rect.x, rect.y);
    if( !m_td_array[array_index] ){
        glTextureDescriptor *p = new glTextureDescriptor();
        
        p->x = rect.x;
        p->y = rect.y;
        p->level_min = g_mipmap_max_level + 1;  // default, nothing loaded
        p->m_colorscheme = global_color_scheme;
        m_td_array[array_index] = p;
    }
    return m_td_array[array_index];
}

static void CreateTexture(GLuint &tex_name, bool b_use_mipmaps)
{
    glGenTextures( 1, &tex_name );
        
//        printf("gentex  %d   rect:  %d %d   index %d\n", ptd->tex_name, rect.x, rect.y, array_index);
    glBindTexture( GL_TEXTURE_2D, tex_name );
        
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );


    if(b_use_mipmaps)
        glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
    else
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

#ifdef __OCPN__ANDROID__
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
#endif
}

bool glTexFactory::BuildTexture(glTextureDescriptor *ptd, int base_level, const wxRect &rect)
{
    bool busy_shown = false;
    
    // the quality is only slightly worse because linear_mipmap_linear
    // is impossible, but still replace the texture data with the
    // correct level data and using only texture level 0

#ifdef ocpnUSE_GLES
    bool b_use_compressed_mipmaps = false;
    bool b_use_uncompressed_mipmaps = false;
#else
    bool b_use_compressed_mipmaps = true; // best possible quality
    // don't use uncompressed mipmaps as they are temporary
    // we upload only the correct level so the image quality is good anyways
    bool b_use_uncompressed_mipmaps = !g_GLOptions.m_bTextureCompression;
#endif    

    // on systems with little memory we can go up a mipmap level
    // here so that the uncompressed size without mipmaps (level+1)
    // is nearly the compressed size with all the compressed mipmaps
    // this way we won't require 5x more video memory than normal while we
    // are generating the compressed textures, when the cache is complete the image
    // becomes clearer as it is replaces with the higher resolution compressed version
    bool b_lowmem = false; // maybe instead decide based on how much texture memory we have
#ifdef ocpnUSE_GLES
    b_lowmem = g_GLOptions.m_bTextureCompression;
#endif
    if(g_GLOptions.m_bTextureCompression &&
       ptd->nGPU_compressed == GPU_TEXTURE_UNCOMPRESSED) {
        // if compressed data became available we blow away the texture
        if(ptd->comp_array[base_level])
            DeleteSingleTexture(ptd);
    }

    // we are done if the data is already in the texture
    if(base_level == ptd->level_min)
        return false;

    if(base_level > ptd->level_min) {
        // if we already have the mipmaps then we can return,
        // but if we need memory we should free the texture and
        // re-upload just the higher levels needed
        bool b_use_mipmaps = ptd->nGPU_compressed == GPU_TEXTURE_COMPRESSED ?
            b_use_compressed_mipmaps : b_use_uncompressed_mipmaps;
        if(b_use_mipmaps) {
            double factor = 0.5; // we should free uncompressed textures earliest
            bool bGLMemCrunch = g_tex_mem_used > (double)(g_GLOptions.m_iTextureMemorySize * 1024 * 1024) * factor;
            if(!bGLMemCrunch)
                return false; // we already have the data in vram
        }
    }    
    
    int status = GetTextureLevel( ptd, rect, base_level, ptd->m_colorscheme );

    bool b_use_mipmaps = COMPRESSED_BUFFER_OK == status ?
        b_use_compressed_mipmaps : b_use_uncompressed_mipmaps;

    DeleteSingleTexture(ptd);
    CreateTexture(ptd->tex_name, b_use_mipmaps);
    ptd->nGPU_compressed = COMPRESSED_BUFFER_OK == status ?
        GPU_TEXTURE_COMPRESSED : GPU_TEXTURE_UNCOMPRESSED;

    if( COMPRESSED_BUFFER_OK == status) {
        int texture_level = 0;
        for(int level = base_level; level < ptd->level_min; level++ ) {
            int size = TextureTileSize(level, true);
            int status = GetTextureLevel( ptd, rect, level, ptd->m_colorscheme );
            int dim = TextureDim(level);
            s_glCompressedTexImage2D( GL_TEXTURE_2D, texture_level,
                                      g_raster_format, dim, dim, 0, size,
                                      ptd->comp_array[level]);

            ptd->tex_mem_used += size;
            g_tex_mem_used += size;
            texture_level++;
            
            if(!b_use_mipmaps)
                break;
        }

        //   Free bitmap memory that has already been uploaded to the GPU
        ptd->FreeMap();
        ptd->FreeComp();
    } else { // COMPRESSED_BUFFER_OK == status
        if (m_newCatalog) {
            // it's an empty catalog or it's not used, odds it's going to be slow
            OCPNPlatform::ShowBusySpinner();
            busy_shown = true;
            m_newCatalog = false;
        }

        //  This level has not been compressed yet, and is not in the cache
#if 1   // perhaps we should eliminate this case
        // and build compressed fxt1 textures one per tick
        if( GL_COMPRESSED_RGB_FXT1_3DFX == g_raster_format &&
            g_GLOptions.m_bTextureCompression) {
            // this version avoids re-uploading the data
            g_glTextureManager->ScheduleJob( this, rect, base_level, true, false, true, true);
            ptd->FreeMap();
            ptd->nGPU_compressed = GPU_TEXTURE_COMPRESSED;
            b_use_mipmaps = b_use_compressed_mipmaps;
            glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
        } else
#endif
        {
            int uc_base_level = base_level;
            if(b_lowmem)
                uc_base_level++;
            int texture_level = 0;
            for(int level = uc_base_level; level < ptd->level_min + b_lowmem; level++) {
                int status = GetTextureLevel( ptd, rect, level, ptd->m_colorscheme );
                int dim = TextureDim(level);
                glTexImage2D( GL_TEXTURE_2D, texture_level, GL_RGB,
                              dim, dim, 0, FORMAT_BITS, GL_UNSIGNED_BYTE, ptd->map_array[level] );
                int size = TextureTileSize(level, false);
                ptd->tex_mem_used += size;
                g_tex_mem_used += size;
                texture_level++;
                
                if(!b_use_mipmaps)
                    break;
            }
        }
    }

    ptd->level_min = base_level;

    // free all mipmaps more than a level less than this
    for( int i = 0; i < base_level-1; i++ ) {
        free( ptd->map_array[i] );
        ptd->map_array[i] = 0;
    }

    if(busy_shown)
        OCPNPlatform::HideBusySpinner();
    
    return true;
}

bool glTexFactory::PrepareTexture( int base_level, const wxRect &rect, ColorScheme color_scheme, int mem_used )
{    
    glTextureDescriptor *ptd = NULL;

    try
    {
    ptd = GetOrCreateTD(rect);

    ptd->m_colorscheme = color_scheme;
    
    //glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE ); // why?
    
    if(!BuildTexture(ptd, base_level, rect))
        glBindTexture( GL_TEXTURE_2D, ptd->tex_name );

    // should we schedule compression?
    if(g_GLOptions.m_bTextureCompression &&
       ptd->nGPU_compressed == GPU_TEXTURE_UNCOMPRESSED) {
        // scheduling at base_level reduces vram usage but is slower overall
        // probably shouldn't be used for caching until it can cache each level
        g_glTextureManager->ScheduleJob( this, rect, 0/*base_level*/,
                                         true, false, true, false);
        if( GL_COMPRESSED_RGB_FXT1_3DFX == g_raster_format )
            glBindTexture( GL_TEXTURE_2D, ptd->tex_name ); // reset texture binding

        // Free the map in ram
        ptd->FreeMap();
    }

#if 0
 /* this is not a good place to call GetMemoryStatus
    function as implemented takes for me 1 millisecond to execute.
    In the worst case zoomed out charts can reach thousands of textures and several seconds to render one frame instead of 20-30fps disabling this here

    Memory is already freed in glTextureManager::FactoryCrunch(double factor)
    so the below is probably not needed.*/

    //   If global memory is getting short, we can crunch here.
    //   All mipmaps >= ptd->level_min have been uploaded to the GPU,
    //   so there is no reason to save the bits forever.
    //   Of course, this means that if the texture is deleted elsewhere, then the bits will need to be
    //   regenerated.  The price to pay for memory limits....
    if (g_memCacheLimit > 0) {
        // GetMemoryStatus is slow on linux
        if(mem_used > g_memCacheLimit * 7 / 10)
            ptd->FreeMap();

        if(mem_used > g_memCacheLimit * 9 / 10)
            ptd->FreeAll();
    }
#endif

//    g_Platform->HideBusySpinner();
    
    return true;
    }   //try
    
    catch (...)
    {
        //      Clean up
        ptd->FreeAll();
        DeleteSingleTexture( ptd );
        return false;
    }
}

void glTexFactory::PrepareTiles(const ViewPort &vp, bool use_norm_vp, ChartBase *chart)
{
    ChartBaseBSB *pChartBSB = dynamic_cast<ChartBaseBSB*>( chart );
    if( !pChartBSB ) 
      return;

    // detect changing north/south polar
    if(vp.m_projection_type == PROJECTION_POLAR) {
        bool north = vp.clat > 0;
        if(m_north != north)
            m_prepared_projection_type = 0;
        m_north = north;
    }

    if(vp.m_projection_type == m_prepared_projection_type)
        return;

    m_prepared_projection_type = vp.m_projection_type;

    double native_scale;
    
    native_scale = pChartBSB->GetNativeScale();

    if(m_tiles)
        for(int i=0 ; i < m_ntex; i++)
            delete m_tiles[i];
    delete [] m_tiles;
    m_tiles = new glTexTile*[m_ntex];

    int tex_dim = g_GLOptions.m_iTextureDimension;

    // split cells for accuracy, much more with larger charts, and toward poles
    // depending on projection of viewport and chart
    // This is a very simplistic algorithm to determine split count, could be greatly improved

    double xsplits, ysplits;
    switch(vp.m_projection_type) {
    case PROJECTION_POLAR:
    case PROJECTION_STEREOGRAPHIC:
    case PROJECTION_ORTHOGRAPHIC:
    case PROJECTION_GNOMONIC:
    case PROJECTION_POLYCONIC:
        xsplits = native_scale / 1000000000.0 * tex_dim; // todo: fix this
//        xsplits /= (1 << base_level); // split less zoomed out
        
        // split more near poles
        if(vp.m_projection_type == PROJECTION_ORTHOGRAPHIC) {
            Extent e;
            pChartBSB->GetChartExtent(&e);
            xsplits = xsplits * wxMax(fabsf(e.NLAT), fabsf(e.SLAT)) / 90;
        }
        
        xsplits = round(xsplits);
        ysplits = 2*xsplits;
        
        xsplits = wxMin(wxMax(xsplits, 1), 8);
        ysplits = wxMin(wxMax(ysplits, 1), 8);
        break;
    case PROJECTION_EQUIRECTANGULAR:
        // needed for skewed charts?
//        xsplits = ysplits = 4;
//        break;
    default:
        xsplits = ysplits = 1; // TODO: is this good enough in all cases to reproject
        // non-mercator charts or even SM_ECC mercator charts in all cases?
    }

    ViewPort nvp;
    if(use_norm_vp) {
        pChartBSB->chartpix_to_latlong(m_size_X/2, m_size_Y/2, &m_clat, &m_clon);
        nvp = glChartCanvas::NormalizedViewPort(vp, m_clat, m_clon);
    }

    //    Using a 2D loop, iterate thru the texture tiles of the chart
    wxRect rect;
    rect.y = 0;
    for( int i = 0; i < m_ny_tex; i++ ) {
        rect.height = tex_dim;
        rect.x = 0;
        for( int j = 0; j < m_nx_tex; j++ ) {
            rect.width = tex_dim;

            glTexTile *tile = m_tiles[i*m_nx_tex + j] = new glTexTile;
            tile->rect = rect;

            double lat, lon;
            float ll[8];
            int x[4] = {rect.x, rect.x, rect.x+rect.width, rect.x+rect.width};
            int y[4] = {rect.y+rect.height, rect.y, rect.y, rect.y+rect.height};

            for(int k=0; k<4; k++) {
                pChartBSB->chartpix_to_latlong(x[k], y[k], &lat, &lon);
                ll[2*k+0] = lon, ll[2*k+1] = lat;
            }

            // resolve idl
            float lonmin = ll[0], lonmax = ll[0];
            float latmin = ll[1], latmax = ll[1];
            for(int i=2; i<8; i+=2) {
                lonmin = wxMin(lonmin, ll[i]), lonmax = wxMax(lonmax, ll[i]);
                latmin = wxMin(latmin, ll[i+1]), latmax = wxMax(latmax, ll[i+1]);
            }

            if(fabsf(lonmin - lonmax) > 180) {
                lonmin = 540, lonmax = 0; 
                for(int i=0; i<8; i+=2) {
                    float lon = ll[i] < 0 ? ll[i]+360 : ll[i];
                    lonmin = wxMin(lonmin, lon), lonmax = wxMax(lonmax, lon);
                }
            }

            tile->box.Set(latmin, lonmin, latmax, lonmax);

            double xs = rect.width / xsplits;
            double ys = rect.height / ysplits;
            double x1 = rect.x, u1 = 0;

            int maxncoords = 4*xsplits*ysplits;
            tile->m_coords = new float[2*maxncoords];
            tile->m_texcoords = new float[2*maxncoords];

            tile->m_ncoords = 0;

            int end = 0; // should be 1<<base_level but we have no way to know now

            for(int x = 0; x<xsplits; x++) {
                double x2 = wxMin(x1+xs, m_size_X - end);
                double u2 = (x2-rect.x)/rect.width;

                double y1 = rect.y, v1 = 0;
                for(int y = 0; y<ysplits; y++) {
                    double y2 = wxMin(y1+ys, m_size_Y - end);
                    double v2 = (y2-rect.y)/rect.height;

                    // todo avoid extra calls per loop and also caching from above
                    double xc[4] = {x1, x1, x2, x2}, yc[4] = {y2, y1, y1, y2};
                    double lat[4], lon[4];
                    for(int k=0; k<4; k++){
                        pChartBSB->chartpix_to_latlong(xc[k], yc[k], lat+k, lon+k);
                    }

                    double u[4] = {u1, u1, u2, u2}, v[4] = {v2, v1, v1, v2};
                    for(int j=0; j<4; j++) {
                        int idx = 2*tile->m_ncoords;
                        tile->m_texcoords[idx+0] = u[j];
                        tile->m_texcoords[idx+1] = v[j];

                        if(use_norm_vp) {
                            wxPoint2DDouble p = nvp.GetDoublePixFromLL(lat[j], lon[j]);
                            tile->m_coords[idx+0] = p.m_x;
                            tile->m_coords[idx+1] = p.m_y;
                        } else {
                            tile->m_coords[idx+0] = lat[j];
                            tile->m_coords[idx+1] = lon[j];
                        }
                        tile->m_ncoords++;
                    }

                    if(y1 + ys > m_size_Y - end)
                        break;
                        
                    v1 = v2;
                    y1 = y2;
                }
                if(x1 + xs > m_size_X - end)
                    break;
                
                u1 = u2;
                x1 = x2;
            }
            rect.x += rect.width;
        }
        rect.y += rect.height;
    }
}


bool glTexFactory::UpdateCacheLevel( const wxRect &rect, int level, ColorScheme color_scheme, unsigned char *data, int size)
{
    if( !g_GLOptions.m_bTextureCompressionCaching)
        return false;

    if(!data)
        return false;

    //  Search for the requested texture
        //  Search the catalog for this particular texture
    CatalogEntryValue *v = GetCacheEntryValue(level, rect.x, rect.y, color_scheme) ;
        
    //      This texture is already done
    if(v != 0)
        return false;
    
    return UpdateCachePrecomp(data, size, rect, level, color_scheme);

}

bool glTexFactory::UpdateCacheAllLevels( const wxRect &rect, ColorScheme color_scheme, unsigned char **compcomp_array, int *compcomp_size)
{
    if( !g_GLOptions.m_bTextureCompressionCaching)
        return false;

    bool work = false;

    for (int level = 0; level < g_mipmap_max_level + 1; level++ )
        work |= UpdateCacheLevel( rect, level, color_scheme, compcomp_array[level], compcomp_size[level] );
    if (work) {
        WriteCatalogAndHeader();
    }    
    
    return work;
}

int glTexFactory::GetTextureLevel( glTextureDescriptor *ptd, const wxRect &rect,
                                   int level, ColorScheme color_scheme )
{
    //  Already available in the texture descriptor?
    if(g_GLOptions.m_bTextureCompression) {
        if( ptd->comp_array[level] )
            return COMPRESSED_BUFFER_OK;
        if( ptd->compcomp_array[level] ) {
            // If we have the compcomp bits in ram decompress them
            int size = TextureTileSize(level, true);
            unsigned char *cb = (unsigned char*)malloc(size);
            LZ4_decompress_fast((char*)ptd->compcomp_array[level], (char*)cb, size);
            ptd->comp_array[level] = cb;
            return COMPRESSED_BUFFER_OK;
        } else if(g_GLOptions.m_bTextureCompressionCaching) {
            //  If cacheing compressed textures, look in the cache
            //  Search for the requested texture
            //  Search the catalog for this particular texture
            CatalogEntryValue *p = GetCacheEntryValue(level, rect.x, rect.y, color_scheme);
        
            //      Requested texture level is found in the cache
            //      so go load it
            if( p != 0 ) {
                int size = TextureTileSize(level, true);

                if(m_fs->IsOpened()){
                    m_fs->Seek(p->texture_offset);
                    ptd->comp_array[level] = (unsigned char*)malloc(size);
                    int max_compressed_size = LZ4_COMPRESSBOUND(g_tile_size);
                    char *compressed_data = (char*)malloc(p->compressed_size);
                    m_fs->Read(compressed_data, p->compressed_size);
                    LZ4_decompress_fast(compressed_data, (char*)ptd->comp_array[level], size);
                    free(compressed_data);
                }
            
                return COMPRESSED_BUFFER_OK;
            }
        }
    }
        
    //  Requested Texture level is not in cache, and not already built
    //  So go build it
    if( !ptd->map_array[level] )
        GetFullMap( ptd, rect, m_ChartPath, level );
        
    return MAP_BUFFER_OK;
}


// return not used
// false? never
// true 
bool glTexFactory::LoadHeader(void)
{
    if(m_hdrOK)
        return true;

    bool need_new = false;

    if(wxFileName::FileExists(m_CompressedCacheFilePath)) {
        
        m_fs = new wxFFile(m_CompressedCacheFilePath, _T("rb+"));
        if(m_fs->IsOpened()){
        
            CompressedCacheHeader hdr;
            
            //  Header is located at the end of the file
            wxFileOffset hdr_offset = m_fs->Length() -sizeof( hdr);
            hdr_offset = m_fs->Seek( hdr_offset );
            
            if( sizeof( hdr) == m_fs->Read(&hdr, sizeof( hdr ))) {
                if( hdr.magic != COMPRESSED_CACHE_MAGIC ||
                    hdr.chartdate != m_chart_date_binary ||
                    hdr.chartfile_date != m_chartfile_date_binary ||
                    hdr.chartfile_size != m_chartfile_size ||
                    hdr.format != g_raster_format) {
                    
                    //  Bad header signature    
                    delete m_fs;
                    need_new = true;
                }
                else {      // good header
                    n_catalog_entries = hdr.m_nentries;
                    m_catalog_offset = hdr.catalog_offset;
                }
            }
            else{  // file exists, and is empty
                n_catalog_entries = 0;
                m_catalog_offset = 0;
                WriteCatalogAndHeader();
            }
        }  // is open
        
        else{               // some problem opening file, probably permissions on Win7
            delete m_fs;
            need_new = true;
            wxRemoveFile(m_CompressedCacheFilePath);
        }
        
    }   // exists
    
    else {   // File does not exist
        wxFileName fn(m_CompressedCacheFilePath);
        if(!fn.DirExists())
            fn.Mkdir();
        need_new = true;
    }

    if (need_new) {
        //  Create new file, with empty catalog, and correct header
        m_fs = new wxFFile(m_CompressedCacheFilePath, _T("wb"));
        n_catalog_entries = 0;
        m_catalog_offset = 0;
        WriteCatalogAndHeader();
        delete m_fs;

        m_fs = new wxFFile(m_CompressedCacheFilePath, _T("rb+"));
    }
    m_hdrOK = true;
    return true;
}

bool glTexFactory::AddCacheEntryValue(const CatalogEntry &p)
{
    if ((int)p.k.tcolorscheme < 0 || p.k.tcolorscheme >= N_COLOR_SCHEMES) 
        return false;

    if (p.k.mip_level < 0 || p.k.mip_level >= MAX_TEX_LEVEL) 
        return false;

    int array_index = ArrayIndex(p.k.x, p.k.y);
    if (array_index < 0 || array_index >= m_ntex) 
        return false;

    if (m_cache[p.k.tcolorscheme][p.k.mip_level] == 0) 
        m_cache[p.k.tcolorscheme][p.k.mip_level] = (CatalogEntryValue* )calloc(m_ntex, sizeof (CatalogEntryValue) );

    CatalogEntryValue *v = m_cache[p.k.tcolorscheme][p.k.mip_level];
    CatalogEntryValue *r = &v[array_index];
    *r = p.v; 
    return true;
}

bool glTexFactory::LoadCatalog(void)
{
    m_newCatalog = false;
    if(m_catalogOK)
        return true;

    if( !LoadHeader() )
        return false;
    
    if (n_catalog_entries == 0) {
        // new empty header
        m_catalogOK = true;
        m_newCatalog = true;
        return true;
    }
    
    m_fs->Seek(m_catalog_offset);
 
    CatalogEntry ps;
    int buf_size =  ps.GetSerialSize();
    unsigned char *buf = (unsigned char *)malloc(buf_size);
    
    CatalogEntry p;
    bool bad = false;
    for(int i=0 ; i < n_catalog_entries ; i++){
        m_fs->Read(buf, buf_size);
        p.DeSerialize(buf);
        if (!AddCacheEntryValue(p))
            bad = true;
    }
    
    free(buf);
    if (bad && !m_catalogCorrupted) {
        wxLogMessage(_T("Bad cache catalog %s %s"), m_ChartPath.c_str(), m_CompressedCacheFilePath.c_str());
        m_catalogCorrupted = true;
    }
    m_catalogOK = true;
    return true;
}


bool glTexFactory::WriteCatalogAndHeader()
{
    if(m_fs && m_fs->IsOpened()){

        m_fs->Seek(m_catalog_offset);
        
        CatalogEntry ps;
        int buf_size =  ps.GetSerialSize();
        unsigned char buf[CATALOG_ENTRY_SERIAL_SIZE];   // MSVC requires constant stack array size...
        int new_n_catalog_entries = 0;
        CatalogEntry p;
        wxRect rect;
        for (int i = 0; i < N_COLOR_SCHEMES; i++) {
            p.k.tcolorscheme = (ColorScheme)i;
            for (int j = 0; j < MAX_TEX_LEVEL; j++) {
                CatalogEntryValue *v = m_cache[i][j];
                if (!v) 
                    continue;
                p.k.mip_level = j;
                for (int k = 0; k < m_ntex; k++) {
                    ArrayXY(&rect, k);
                    p.k.y = rect.y;
                    p.k.x = rect.x;
                    CatalogEntryValue *r = &v[k];
                    if (r->compressed_size == 0)
                        continue;
                    p.v = *r;
                    new_n_catalog_entries++;
                    p.Serialize(buf);
                    m_fs->Write( buf, buf_size);

                }
            }
        }

        n_catalog_entries = new_n_catalog_entries;
        //   Write header at file end
        CompressedCacheHeader hdr;
        hdr.magic = COMPRESSED_CACHE_MAGIC;
        hdr.format = g_raster_format;
        hdr.m_nentries = n_catalog_entries;
        hdr.catalog_offset = m_catalog_offset;
        hdr.chartdate = m_chart_date_binary;
        hdr.chartfile_date = m_chartfile_date_binary;
        hdr.chartfile_size = m_chartfile_size;
        
        m_fs->Write( &hdr, sizeof(hdr));
        m_fs->Flush();
        
        return true;
    }
    else
        return false;
}

bool glTexFactory::UpdateCachePrecomp(unsigned char *data, int data_size, const wxRect &rect,
                                      int level, ColorScheme color_scheme, bool write_catalog)
{
    if (level < 0 || level >= MAX_TEX_LEVEL)
        return false;	// XXX BUG

    //  Search the catalog for this particular texture
    if (GetCacheEntryValue(level, rect.x, rect.y, color_scheme) != 0) 
        return false;
    
    // Make sure the file exists
    wxASSERT(m_fs != 0);
        
    if( ! m_fs->IsOpened() )
        return false;

    //      Create a new catalog entry
    CatalogEntry p( level, rect.x, rect.y, color_scheme);
    
    //      Write the compressed data to disk
    p.v.texture_offset = m_catalog_offset;
    
    p.v.compressed_size = data_size;
    AddCacheEntryValue(p);            
    n_catalog_entries++;

    //      We write the new data at the current catalog offset, overwriting the old catalog
    m_fs->Seek( m_catalog_offset );
    m_fs->Write( data, data_size );
    
    //      Write the catalog and Header (which follows the catalog at the end of the file
    m_catalog_offset += data_size;
    if (write_catalog)
        WriteCatalogAndHeader();
    
    return true;
}
