/*
 * Copyright (C) 2017.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * 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.
 */ 
package uk.me.parabola.imgfmt.app.dem;

import java.io.ByteArrayOutputStream;

import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.mkgmap.reader.hgt.HGTReader;

/**
 * This keeps the bit stream data and header info for a single DEM tile.
 * A tile contains the height values for a number of points organised in a width * height matrix.
 * The values of the matrix are not stored directly. Instead, a compression algorithm is used to reduce
 * the size of the needed bit stream. The algorithm uses different methods, mainly a delta encoding, run length encoding
 * and a variable number of bits and encoding methods to store the actual value.
 * The actually used methods are not stored, instead they are predicted from previously stored values (if any) and header data. 
 * Both the encoder and decoder have to implement exactly the same prediction algorithm.
 * As of 2017-12-13 only a subset of the algorithms used by Garmin is understood, but it seems to be enough to store valid data.
 * 
 * This code is based on the program and documentation created by Frank Stinner. 
 * 
 * @author Gerd Petermann
 *
 */
public class DEMTile {
	private ByteArrayOutputStream bits;
	private int[] heights;
	private final int height;
	private final int width;
	private int offset;					// offset from section.dataOffset2
	private final int baseHeight;		// base or minimum height in this tile 
	private final int maxDeltaHeight;	// delta between max height and base height
	private final int encodingType;  	// determines how the highest values are displayed 
	private final boolean hasData;		// not all voids

	private int bitPos;
	private byte currByte;
	private int currPlateauTablePos; // current position in plateau tables
	private CalcType currCalcType;
	
	private final static boolean DEBUG = false;
	private StringBuilder bs;
	

	// fields used for debugging
	private final int tileNumberLat;
	private final int tileNumberLon;

	private enum EncType {
		HYBRID, LEN
	}

	private enum WrapType {
		WRAP_0, WRAP_1, WRAP_2
	}
	
	private enum CalcType {
		CALC_P_LEN, CALC_STD, CALC_PLATEAU_ZERO, CALC_PLATEAU_NON_ZERO
	}

	static final int[] plateauUnit = { 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 8, 8, 8, 8, 16, 16, 32, 32, 64, 64, 128 };
	static final int[] plateauBinBits = { 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 8 };

	public DEMTile (int col, int row, int width, int height, short[] realHeights) {
		this.width = width;
		this.height = height;
		this.tileNumberLon = col;
		this.tileNumberLat = row;
		
		// check values in matrix
		int min = Integer.MAX_VALUE;
		int max = Integer.MIN_VALUE;
		int countInvalid = 0;
		for (int h : realHeights) {
			if (h == HGTReader.UNDEF)
				++countInvalid;
			else {
				if (h > max)
					max = h;
				if (h < min)
					min = h;
			}
		}
		if (min == Integer.MAX_VALUE) {
			// all values are invalid 
			hasData = false;
			encodingType = 2;
			min = 0;
			max = 0;
		} else if (countInvalid > 0) {
			// some values are invalid
			hasData = true;
			encodingType = 2; // don't display highest value 
			max++;
		} else {
			// all height values are valid
			hasData = true;
			encodingType = 0;
		}

		this.baseHeight = min;
		this.maxDeltaHeight = max - min;
		if (min == max) {
			return; // all heights equal 
		}
		createBitStream(realHeights);
	}
	
	public boolean hasValidHeights() {
		return hasData;
	}

	public int getBaseHeight() {
		return baseHeight;
	}

	public int getMaxHeight() {
		return baseHeight + maxDeltaHeight - (encodingType == 0 ? 0 : 1); 
	}
	
	private void createBitStream(short[] realHeights) {
		bits = new ByteArrayOutputStream(1024); 
		heights = new int[realHeights.length];
		// normalise the height matrix
		for (int i = 0; i < realHeights.length; i++) {
			if (realHeights[i] == HGTReader.UNDEF)
				heights[i] = maxDeltaHeight;
			else 
				heights[i] = (realHeights[i] - baseHeight);
		}
		// all values in heights are now expected to be between 0 .. maxDeltaHeight
		if (DEBUG) {
			bs = new StringBuilder();
		}
		encodeDeltas();
		// cleanup 
		bs = null;
		heights = null;
	}
	
	private void addBit(boolean bit) {
		if (DEBUG) {
			bs.append(bit ? '1':'0');
		}
		if (bit) {
			currByte |= 1 << (7-bitPos);
		}
		bitPos++;
		if (bitPos > 7) {
			bitPos = 0;
			bits.write(currByte);
			currByte = 0;
		}
	}
	
	/**
	 * The main loop to calculate the bit stream data.
	 */
	private void encodeDeltas() {
		int pos = 0;
		currCalcType = null;
		ValPredicter encStandard = new ValPredicter(CalcType.CALC_STD, maxDeltaHeight);
		ValPredicter encPlateauF0 = new ValPredicter(CalcType.CALC_PLATEAU_ZERO, maxDeltaHeight);
		ValPredicter encPlateauF1 = new ValPredicter(CalcType.CALC_PLATEAU_NON_ZERO, maxDeltaHeight);
		ValPredicter encoder = null;
		boolean writeFollower = false;
		while (pos < heights.length) {
			if (DEBUG) {
				bs.setLength(0);
			}
			int n = pos % width;
			int m = pos / width;
			int hUpper = getHeight(n, m - 1);
			int hLeft = getHeight(n - 1, m);
			int dDiff = hUpper - hLeft;
			if (writeFollower) {
				encoder = (dDiff == 0) ? encPlateauF0 : encPlateauF1;
				writeFollower = false;
			} else if (dDiff == 0) {
				currCalcType = CalcType.CALC_P_LEN;
				int pLen = calcPlateauLen(n, m);
				writePlateauLen(pLen, n);
				pos += pLen;
				writeFollower = (pos % width != 0 || pLen == 0);
				continue;
			} else {
				encoder = encStandard;
			}
			currCalcType = encoder.type;
			encoder.setDDiff(dDiff);
			int v;
			int h = getHeight(n, m);
			if (currCalcType == CalcType.CALC_STD) {
				int predict;
				int hUpLeft = getHeight(n-1, m-1);
				int hdiffUp = hUpper- hUpLeft;
				if (hdiffUp >= maxDeltaHeight - hLeft) {
					predict = -1;
				} else if (hdiffUp <= -hLeft) {
					predict = 0; 
				} else {
					predict = hLeft + hdiffUp;
				}
				if (dDiff > 0)
					v = -h + predict;
				else 
					v = h -predict;
				
			} else {
				// plateau follower: predicted value is upper height 
				v = h - hUpper;
			}
			if (DEBUG) {
				bs.setLength(0);
			}
			encoder.write(v);
			pos++;
		}
		if (bitPos > 0)
			bits.write(currByte);
	}

	/**
	 * Add the bits for a given plateau length using the current value of lastTablePos 
	 * @param pLen
	 */
	private void writePlateauLen(int pLen, int col) {
		int len = pLen;
		int x = col;

		if (col + len >= width) {
			// this is not really needed but sometimes produces fewer bits
			// compared to the loop in the else branch
			while (x < width) {
				int unit = plateauUnit[currPlateauTablePos++];
				len -= unit;
				x += unit;
				addBit(true);
			}
			if (x != width) {
				currPlateauTablePos--;
			}
		} else {
			while (true) {
				int unit = plateauUnit[currPlateauTablePos];
				if (len < unit)
					break;
				currPlateauTablePos++;
				len -= unit;
				addBit(true);
				x += unit;
				if (x > width)
					currPlateauTablePos--;
				if (x >= width) {
					return;
				}
			}
			if (currPlateauTablePos > 0)
				currPlateauTablePos--;
			
			addBit(false); // separator bit
			int binBits = plateauBinBits[currPlateauTablePos];
			if (binBits > 0) {
				writeValAsBin(Math.abs(len), binBits);
			}
		}
		
	}

	/**
	 * Write an unsigned binary value with the given number of bits, MSB first. 
	 * @param val
	 * @param numBits
	 */
	private void writeValAsBin(int val, int numBits) {
		if (numBits == 0 && val == 0)
			return;
		
		int t = 1 << (numBits - 1);
		if (val >= t << 1)
			throw new MapFailedException("Number too big for binary encoding with " + numBits + " bits:" + val);
		while (t > 0) {
			addBit((val & t) != 0);
			t >>= 1;
		}
	}

	/**
	 * Write a length encoded value as a sequence of 0-bits followed by a 1-bit. 
	 * @param val
	 */
	private void writeNumberOfZeroBits(int val) {
		for (int i = 0; i < val; i++)
			addBit(false);
		addBit(true); // terminating 1-bit
	}

	/**
	 * Write an unsigned binary value with the given number of bits, MSB first. 
	 * @param val
	 * @param hunit
	 * @param maxZeroBits
	 * @return 
	 */
	private boolean writeValHybrid(int val, int hunit, int maxZeroBits) {
		assert hunit > 0;
		assert Integer.bitCount(hunit) == 1;
		int numBits = Integer.numberOfTrailingZeros(hunit);
		int binPart;
		int lenPart;
		if (val > 0) {
			binPart = (val - 1) % hunit;
			lenPart = (val - 1 - binPart) / hunit;
		} else {
			binPart = -val % hunit;
			lenPart = (-val - binPart) / hunit;
		}
		if (lenPart <= maxZeroBits) {
			writeNumberOfZeroBits(lenPart); // write length encoded part
			writeValAsBin(binPart, numBits); // write binary encoded part
			addBit(val > 0); // sign bit, 1 means positive
			return true;
		}
		return false;
	}

	/**
	 * Write a signed binary value with the given number of bits, MSB first, sign bit last 
	 * @param val
	 * @param numZeroBits number of zero bits that is considered valid 
	 */
	private void writeValBigBin (int val, int numZeroBits) {
		// signal big bin by writing an invalid number of zero bits
		writeNumberOfZeroBits(numZeroBits + 1);
		int bits = getBigBinBits(maxDeltaHeight);
		if (val < 0)
			writeValAsBin(-val - 1, bits - 1);
		else
			writeValAsBin(val - 1, bits - 1);
		addBit(val <= 0); // sign bit, 0 means positive
	}

	/**
	 * A plateau is a sequence of equal delta values. Calculate the length.
	 * @param col current column
	 * @param row current row
	 * @return the length of the plateau, which might be zero.
	 */
	private int calcPlateauLen(int col, int row) {
		int len = 0;
		int v = getHeight(col-1, row);
		while (col + len < width) {
			if (v == getHeight(col + len, row)) {
				++len;
			} else 
				break;
			
		}
		return len;
	}

	private int getHeight(int col, int row) {
		if (heights == null)
			return 0;
		if (row < 0) {
			// virtual 1st row
			return 0;
		}
		if (col < 0) {
			return row == 0 ? 0 : heights[(row - 1) * width]; 
		}
		return heights[col + row * width];
	}

	/**
	 * Write tile header with the given field sizes 
	 * @param writer
	 * @param recordDesc gives the field sizes
	 */
	public void writeHeader(ImgFileWriter writer, int recordDesc) {
		if (maxDeltaHeight == 0)
			offset = 0;
		int offsetSize = (recordDesc & 0x3) + 1;
		int baseSize = ((recordDesc & 0x4) >> 2) + 1;
		int deltaSize = ((recordDesc & 0x8) >> 3) + 1;
		boolean hasExtra = (recordDesc & 0x10) != 0;
		writer.putNu(offsetSize, offset);
		if (baseSize == 1)
			writer.put1s(baseHeight);
		else 
			writer.put2s(baseHeight);
		writer.putNu(deltaSize, maxDeltaHeight);
		if (hasExtra)
			writer.put1u(encodingType);
	}

	public void writeBitStreamData(ImgFileWriter writer) {
		if (bits != null) {
			writer.put(bits.toByteArray());
		}
	}
	
	private static int evaluateData(int oldsum, int elemcount, int newdata, int region) {
		switch (region) {
		case 0:
			return -1 - oldsum - elemcount;
		case 1:
			return 2 * (newdata + elemcount) + 3;
		case 2:
			return 2 * newdata - 1;
		case 3:
			return 2 * (newdata - elemcount) - 5;
		default:
			return 1 - oldsum + elemcount;
		}
	}

	private static int getEvaluateDataRegion(int oldsum, int elemcount, int newdata) {
		if (elemcount < 63) {
			if (newdata < -2 - ((oldsum + 3 * elemcount) >> 1)) {
				return 0;
			} else if (newdata < -((oldsum + elemcount) >> 1)) {
				return 1;
			} else if (newdata < 2 - ((oldsum - elemcount) >> 1)) {
				return 2;
			} else if (newdata < 4 - ((oldsum - 3 * elemcount) >> 1)) {
				return 3;
			} else {
				return 4;
			}
		} else {
			if (newdata < -2 - ((oldsum + 3 * elemcount) >> 1)) {
				return 0;
			} else if (newdata < -((oldsum + elemcount) >> 1) - 1) {
				// special case in if !
				return 1;
			} else if (newdata < 2 - ((oldsum - elemcount) >> 1)) {
				return 2;
			} else if (newdata < 4 - ((oldsum - 3 * elemcount) >> 1)) {
				return 3;
			} else {
				return 4;
			}
		}
	}

	/**
	 * This class keeps statistics about the previously encoded values and tries to predict the next value.
	 * It also calculates the encoding method to use. 
	 * Based on findings of Frank Stinner. 
	 */
	private class ValPredicter {
		private EncType encType;
		private WrapType wrapType;
		private int sumH;
		private int sumL;
		private int elemCount;
		private int hunit;
		private final CalcType type;
		private final int unitDelta;
		private int dDiff;
		private final int maxZeroBits;
		final int l0WrapUp, l0WrapDown, l1WrapUp, l1WrapDown, l2WrapUp, l2WrapDown,hWrapUp, hWrapDown;
		
		public ValPredicter(CalcType type, int maxHeight) {
			super();
			this.type = type;
			int numZeroBits = getMaxLengthZeroBits(maxHeight);
			if (type == CalcType.CALC_PLATEAU_NON_ZERO || type == CalcType.CALC_PLATEAU_ZERO)
				--numZeroBits;
			maxZeroBits = numZeroBits;
			
			unitDelta = Math.max(0, maxHeight - 0x5f) / 0x40; // TODO what does it mean? 
			encType = EncType.HYBRID;
			wrapType = WrapType.WRAP_0;
			hunit = getStartHUnit(maxHeight);

			// calculate threshold values for wrapping
			if (maxHeight % 2 == 0) {
				l0WrapDown = maxHeight / 2;
				l0WrapUp = -maxHeight / 2;
				l1WrapDown = (maxHeight + 2) / 2;
				l1WrapUp = -maxHeight / 2;
				l2WrapDown = maxHeight / 2;
				l2WrapUp = -maxHeight / 2;
			} else {
				l0WrapDown = (maxHeight + 1) / 2;
				l0WrapUp = -(maxHeight - 1) / 2;
				l1WrapDown = (maxHeight + 1) / 2;
				l1WrapUp = -(maxHeight - 1) / 2;
				l2WrapDown = (maxHeight - 1) / 2;
				l2WrapUp = -(maxHeight + 1) / 2;
			}

			hWrapDown = (maxHeight + 1) / 2;
			hWrapUp = -(maxHeight - 1) / 2;
		}

		/**
		 * This 
		 * @param data
		 * @return
		 */
		private int wrap(int data) {
			int v = data;
			int down,up;
			if (encType == EncType.HYBRID) {
				down = hWrapDown;
				up = hWrapUp;
			} else {
				if (wrapType == WrapType.WRAP_0) {
					down = l0WrapDown;
					up = l0WrapUp;
				} else if (wrapType == WrapType.WRAP_1) {
					down = l1WrapDown;
					up = l1WrapUp;
				} else {
					down = l2WrapDown;
					up = l2WrapUp;
				}
			}
			
			if (v > down)
				v = v - (maxDeltaHeight + 1);
			if (v < up)
				v = v + maxDeltaHeight + 1;
			return v; 
		}
		
		public void write(int val) {
			
			int wrapped = wrap(val);
			int delta1 = wrapped;
			if (type == CalcType.CALC_PLATEAU_ZERO) {
				if (delta1 <= 0)
					delta1++;
			} else if (type == CalcType.CALC_PLATEAU_NON_ZERO) {
				if (dDiff > 0) {
					delta1 = -delta1; 
				}
			}
			int delta2;
			if (wrapType == WrapType.WRAP_0)
				delta2 = delta1;
			else if (wrapType == WrapType.WRAP_1)
				delta2 = 1 - delta1;
			else delta2 = -delta1;
			boolean written = false;
			if (encType == EncType.HYBRID) {
				written = writeValHybrid(delta2, hunit, getCurrentMaxZeroBits());
			} else {
				// EncType.LEN 
				// 2 * Math.Abs(data) - (Math.Sign(data) + 1) / 2
				int n0;
				if (delta2 < 0) {
					n0 = -delta2 * 2;
				} else if (delta2 > 0){
					n0 = (delta2 -1) * 2 + 1;
				} else { 
					n0 = 0;
				}
				if (n0 <= getCurrentMaxZeroBits()) {
					writeNumberOfZeroBits(n0);
					written = true;
				}
			}
			if (!written)
				writeValBigBin(delta2, getCurrentMaxZeroBits());
			processVal(delta1);
		}

		/**
		 * This looks wrong but seems to be needed. For a plateau follower we reduce the max value.
		 * The effect seems to be that a Big Bin is written although it would fit in the normal number.
		 * Maybe a BigBin signals that a flag should be reset or currPlateauTablePos is too high?
		 * @return number of 0 bits that are considered okay.
		 */
		int getCurrentMaxZeroBits() {
			if (currCalcType == CalcType.CALC_PLATEAU_NON_ZERO || currCalcType == CalcType.CALC_PLATEAU_ZERO)
				return maxZeroBits - plateauBinBits[currPlateauTablePos];
			return maxZeroBits;
		}
		
		private void processVal(int delta1) {
			if (type == CalcType.CALC_STD) {
				// calculate threshold sum hybrid 
				sumH += delta1 > 0 ? delta1 : -delta1;
				if (sumH + unitDelta  + 1 >= 0xffff)
					sumH -= 0x10000;

				// calculate threshold sum for length encoding
				int evalRegion = -1;
				int workData = delta1;
				if (elemCount == 63) {
					evalRegion = getEvaluateDataRegion(sumL, elemCount, delta1);
					boolean datagerade = delta1 % 2 == 0;
					boolean sumL1 = (sumL - 1) % 4 == 0;

					switch (evalRegion) {
					case 0:
					case 2:
					case 4:
						if ((sumL1 && !datagerade) || (!sumL1 && datagerade)) {
							workData++;
						}
						break;
					case 1:
						workData++;
						if ((sumL1 && !datagerade) || (!sumL1 && datagerade)) {
							workData++;
						}
						break;
					case 3:
						if ((sumL1 && datagerade) || (!sumL1 && !datagerade)) {
							workData--;
						}
						break;
					}
				}
				if (evalRegion < 0)
					evalRegion = getEvaluateDataRegion(sumL, elemCount, workData);
				int eval = evaluateData(sumL, elemCount, workData, evalRegion);
				sumL += eval;

				// now update elem counter
				elemCount++;

				if (elemCount == 64) {
					elemCount = 32;
					sumH = ((sumH - unitDelta) >> 1) - 1;
					
					sumL /= 2;
				}

				// calculate new hunit
				hunit = normalizeHUnit((unitDelta + sumH + 1) / (elemCount + 1));

				// finally determine encode type for next value
				wrapType = WrapType.WRAP_0;
				if (hunit > 0) {
					encType = EncType.HYBRID;
				} else {
					encType = EncType.LEN;
					if (sumL > 0)
						wrapType = WrapType.WRAP_1;
				}
			} else if (type == CalcType.CALC_PLATEAU_ZERO) {
				// calculate threshold sum hybrid 
				sumH += delta1 > 0 ? delta1 : 1 - delta1; // different to standard
				if (sumH + unitDelta  + 1 >= 0xffff)
					sumH -= 0x10000;
				
				// calculate threshold sum for length encoding
				sumL += delta1 <= 0 ? -1 : 1;
				elemCount++;
				
				if (elemCount == 64) {
					elemCount = 32;
					sumH = ((sumH - unitDelta) >> 1) - 1;
					sumL /= 2;
					if (sumL % 2 != 0) {
						sumL++;
					}
				}

				// calculate new hunit
				hunit = normalizeHUnit((unitDelta + sumH + 1 - elemCount / 2) / (elemCount + 1));
				// finally determine encode type for next value
				wrapType = WrapType.WRAP_0;
				if (hunit > 0) {
					encType = EncType.HYBRID;
				} else {
					encType = EncType.LEN;
					if (sumL >= 0)
						wrapType = WrapType.WRAP_1;
				}
			} else {
				assert type == CalcType.CALC_PLATEAU_NON_ZERO;
				// calculate threshold sum hybrid 
				sumH += delta1 < 0 ? -delta1 : delta1; // simple absolute sum
				if (sumH + unitDelta  + 1 >= 0xffff)
					sumH -= 0x10000;

				// calculate threshold sum for length encoding
				sumL += delta1 <= 0 ? -1 : 1;
				elemCount++;
				
				if (elemCount == 64) {
					elemCount = 32;
					sumH = ((sumH - unitDelta) >> 1) - 1;
					sumL /= 2;
					if (sumL % 2 != 0) {
						sumL--; // different to CALC_PLATEAU_ZERO !
					}
				}

				// calculate new hunit
				hunit = normalizeHUnit((unitDelta + sumH + 1) / (elemCount + 1));
				// finally determine encode type for next value
				wrapType = WrapType.WRAP_0;
				if (hunit > 0) {
					encType = EncType.HYBRID;
				} else {
					encType = EncType.LEN;
					if (sumL <= 0)
						wrapType = WrapType.WRAP_2;
				}
			}
		}

		public void setDDiff(int dDiff) {
			this.dDiff = dDiff; 
		}
	}

	/**
	 * Calculate the longest sequence of zero bits that is considered as a valid number.
	 * The number depends on the field maxDeltaHeight in the header structure.
	 * @param maxHeight
	 * @return the number of bits
	 */
	private static int getMaxLengthZeroBits(int maxHeight) {
		if (maxHeight < 2)
			return 15;
		if (maxHeight < 4)
			return 16;
		if (maxHeight < 8)
			return 17;
		if (maxHeight < 16)
			return 18;
		if (maxHeight < 32)
			return 19;
		if (maxHeight < 64)
			return 20;
		if (maxHeight < 128)
			return 21;
		if (maxHeight < 256)
			return 22;
		if (maxHeight < 512)
			return 25;
		if (maxHeight < 1024)
			return 28;
		if (maxHeight < 2048)
			return 31;
		if (maxHeight < 4096)
			return 34;
		if (maxHeight < 8192)
			return 37;
		if (maxHeight < 16384)
			return 40;
		return 43;
	}

	/**
	 * Calculate the start hunit value. 
	 * The number depends on the field maxDeltaHeight in the header structure.
	 * @param maxHeight
	 * @return the hunit value 
	 */
	private static int getStartHUnit(int maxHeight) {
		if (maxHeight < 0x9f)
			return 1;
		else if (maxHeight < 0x11f)
			return 2;
		else if (maxHeight < 0x21f)
			return 4;
		else if (maxHeight < 0x41f)
			return 8;
		else if (maxHeight < 0x81f)
			return 16;
		else if (maxHeight < 0x101f)
			return 32;
		else if (maxHeight < 0x201f)
			return 64;
		else if (maxHeight < 0x401f)
			return 128;
		return 256;
	}

	static int getBigBinBits(int maxHeight) {
		int bits;
		if (maxHeight <16384) {
			int n = Integer.highestOneBit(maxHeight);
			bits = Integer.numberOfTrailingZeros(n) + 1;
		} else 
			bits = 15;
		return bits;
//		if (maxHeight < 2)
//			return 1;
//		else if (maxHeight < 4)
//			return 2;
//		else if (maxHeight < 8)
//			return 3;
//		else if (maxHeight < 16)
//			return 4;
//		else if (maxHeight < 32)
//			return 5;
//		else if (maxHeight < 64)
//			return 6;
//		else if (maxHeight < 128)
//			return 7;
//		else if (maxHeight < 256)
//			return 8;
//		else if (maxHeight < 512)
//			return 9;
//		else if (maxHeight < 1024)
//			return 10;
//		else if (maxHeight < 2048)
//			return 11;
//		else if (maxHeight < 4096)
//			return 12;
//		else if (maxHeight < 8192)
//			return 13;
//		else if (maxHeight < 16384)
//			return 14;
//		else
//			return 15;
	}

	/**
	 * The hunit value must be a positive multiple of 2 (including 0).  
	 * @param hu the result of a division to calculate the new hunit
	 * @return the normalised value
	 */
	private static int normalizeHUnit(int hu) {
		if (hu > 0) {
			return Integer.highestOneBit(hu);
		}
		return 0;
	}

	public int getBitStreamLen() {
		if (bits == null)
			return 0;
		else 
			return bits.size();
	}

	public void setOffset(int off) {
		this.offset = off;
	}

	@Override
	public String toString() {
		return tileNumberLat + " " + tileNumberLon + " w=" + width + " h=" + height;
	}

	public int getMaxDeltaHeight() {
		return maxDeltaHeight;
	}

	public int getEncodingType() {
		return encodingType;
	}

	public byte[] getBitStream() {
		return bits.toByteArray();
	}
}
