package org.dynmap;
import java.io.File;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.io.IOException;
import java.util.zip.CRC32;

import org.dynmap.utils.LRULinkedHashMap;

/**
 * Image hash code manager - used to reduce compression and notification of updated tiles that do not actually yield new content
 *
 */
public class TileHashManager {
    private File    tiledir;    /* Base tile directory */    
    private boolean enabled;
    
    /**
     * Each tile hash file is a 32x32 tile grid, with each file having a CRC32 hash code generated from its pre-compression frame buffer
     */
    private static class TileHashFile {
        final String key;
        final String subtype;
        final int x;  /* minimum tile coordinate / 32 */
        final int y;  /* minimum tile coordinate / 32 */
        private File hf;
        TileHashFile(String key, String subtype, int x, int y) {
            this.key = key;
            if(subtype != null)
                this.subtype = subtype;
            else
                this.subtype = "";
            this.x = x;
            this.y = y;
        }
        @Override
        public boolean equals(Object o) {
            if(!(o instanceof TileHashFile))
                return false;
            TileHashFile fo = (TileHashFile)o;
            return (x == fo.x) && (y == fo.y) && key.equals(fo.key) && (subtype.equals(fo.subtype)); 
        }
        @Override
        public int hashCode() {
            return key.hashCode() ^ subtype.hashCode() ^ (x << 16) ^ y;
        }
        
        public File getHashFile(File tiledir) {
            if(hf == null) {
                String k;
                int idx = key.indexOf('.'); /* Find first '.' - world name split */
                if(idx > 0)
                    k = key.substring(0, idx) + File.separatorChar + key.substring(idx+1);
                else
                    k = key;
                hf = new File(tiledir, k + (subtype.equals("")?"":("." + subtype)) + "_" + x + "_" + y + ".hash");
            }
            return hf;
        }
        /* Write to file */
        public void writeToFile(File tiledir, byte[] crcbuf) {
            try {
                RandomAccessFile fd = new RandomAccessFile(getHashFile(tiledir), "rw");
                fd.seek(0);
                fd.write(crcbuf);
                fd.close();
            } catch (IOException iox) {
                Log.severe("Error writing hash file - " + getHashFile(tiledir).getPath());
            }
        }
        
        /* Read from file */
        public void readFromFile(File tiledir, byte[] crcbuf) {
            try {
                RandomAccessFile fd = new RandomAccessFile(getHashFile(tiledir), "r");
                fd.seek(0);
                fd.read(crcbuf);
                fd.close();
            } catch (IOException iox) {
                Arrays.fill(crcbuf, (byte)0xFF);
                writeToFile(tiledir, crcbuf);
            }
        }
        /* Read CRC */
        public long getCRC(int tx, int ty, byte[] crcbuf) {
            int off = (128 * (ty & 0x1F)) + (4 * (tx & 0x1F));
            long crc = 0;
            for(int i = 0; i < 4; i++)
                crc = (crc << 8) + (0xFF & (int)crcbuf[off+i]);
            return crc;
        }
        /* Set CRC */
        public void setCRC(int tx, int ty, byte[] crcbuf, long crc) {
            int off = (128 * (ty & 0x1F)) + (4 * (tx & 0x1F));
            for(int i = 0; i < 4; i++)
                crcbuf[off+i] = (byte)((crc >> ((3-i)*8)) & 0xFF);
        }
    }
    
    private static final int MAX_CACHED_TILEHASHFILES = 25;
    private Object lock = new Object();
    private LRULinkedHashMap<TileHashFile, byte[]> tilehash = new LRULinkedHashMap<TileHashFile, byte[]>(MAX_CACHED_TILEHASHFILES);
    private CRC32 crc32 = new CRC32();
    private byte[] crcworkbuf = new byte[8192];
    
    public TileHashManager(File tileroot, boolean enabled) {
        tiledir = tileroot;
        this.enabled = enabled;
    }
    
    /* Read cached hashcode for given tile */
    public long getImageHashCode(String key, String subtype, int tx, int ty) {
        if(!enabled) {
            return -1;  /* Return value that never matches */
        }
        TileHashFile thf = new TileHashFile(key, subtype, tx >> 5, ty >> 5);
        synchronized(lock) {
            byte[] crcbuf = tilehash.get(thf);  /* See if we have it cached */
            if(crcbuf == null) {    /* If not in cache, load it */
                crcbuf = new byte[32*32*4]; /* Get our space */
                Arrays.fill(crcbuf, (byte)0xFF);    /* Fill with -1 */
                tilehash.put(thf, crcbuf);  /* Add to cache */
                thf.readFromFile(tiledir, crcbuf);
            }
            return thf.getCRC(tx & 0x1F, ty & 0x1F, crcbuf);
        }
    }
    /* Calculate hash code for given buffer */
    public long calculateTileHash(int[] newbuf) {
        if(!enabled) {
            return 0;   /* Return value that doesn't match */
        }
        synchronized(lock) {
            if(crcworkbuf.length < (4*newbuf.length)){
                crcworkbuf = new byte[4*newbuf.length];
            }
            for(int i = 0, off = 0; i < newbuf.length; i++) {
                int v = newbuf[i];
                crcworkbuf[off++] = (byte)v;
                crcworkbuf[off++] = (byte)(v>>8);
                crcworkbuf[off++] = (byte)(v>>16);
                crcworkbuf[off++] = (byte)(v>>24);
            }
            /* Calculate CRC-32 for buffer */
            crc32.reset();
            crc32.update(crcworkbuf, 0, 4*newbuf.length);
            return crc32.getValue();
        }
    }
    /* Update hashcode for given tile */
    public void updateHashCode(String key, String subtype, int tx, int ty, long newcrc) {
        if(!enabled)
            return;
        synchronized(lock) {
            /* Now, find and check existing value */
            TileHashFile thf = new TileHashFile(key, subtype, tx >> 5, ty >> 5);
            byte[] crcbuf = tilehash.get(thf);  /* See if we have it cached */
            if(crcbuf == null) {    /* If not in cache, load it */
                crcbuf = new byte[32*32*4]; /* Get our space */
                tilehash.put(thf, crcbuf);  /* Add to cache */
                thf.readFromFile(tiledir, crcbuf);
            }
            thf.setCRC(tx & 0x1F, ty & 0x1F, crcbuf, newcrc);   /* Update field */
            thf.writeToFile(tiledir, crcbuf);   /* And write it out */
        }
    }    
}

@@ -1690,17 +1690,21 @@
 x = key.
-i
+lastI
 ndexOf('
@@ -1720,11 +1720,10 @@
 ind 
-fir
+la
 st '
@@ -1743,16 +1743,44 @@
 me split
+ (allows dots in world name)
  */%0A    

Fix problem with hashfile code handling dots/periods in world names