/*
 * Copyright (C) 2006 Steve Ratcliffe
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License 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.
 * 
 * 
 * Author: Steve Ratcliffe
 * Create date: 26-Nov-2006
 */
package uk.me.parabola.imgfmt.sys;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;

import uk.me.parabola.imgfmt.FileSystemParam;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.labelenc.CharacterEncoder;
import uk.me.parabola.imgfmt.app.labelenc.CodeFunctions;
import uk.me.parabola.imgfmt.app.labelenc.EncodedText;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.log.Logger;

import static java.util.Arrays.asList;

/**
 * The header at the very beginning of the .img filesystem.  It has the
 * same signature as a DOS partition table, although I don't know
 * exactly how much the partition concepts are used.
 *
 * @author Steve Ratcliffe
 */
class ImgHeader {
	private static final Logger log = Logger.getLogger(ImgHeader.class);

	// Offsets into the header.
	private static final int OFF_XOR = 0x0;
	private static final int OFF_VERSION = 0x8;
	private static final int OFF_UPDATE_MONTH = 0xa;
	private static final int OFF_UPDATE_YEAR = 0xb; // +1900 for val >= 0x63, +2000 for less
	private static final int OFF_SUPP = 0xe;		// Appears to be set for gmapsupp files
	private static final int OFF_CHECKSUM = 0xf;
	private static final int OFF_SIGNATURE = 0x10;
	private static final int OFF_UNK_1 = 0x17;

	// If this was a real boot sector these would be the meanings
	private static final int OFF_SECTORS = 0x18;
	private static final int OFF_HEADS = 0x1a;
	private static final int OFF_CYLINDERS = 0x1c;

	private static final int OFF_CREATION_DATE = 0x39;

	// The block number where the directory starts.
	private static final int OFF_DIRECTORY_START_BLOCK = 0x40;

	private static final int OFF_MAP_FILE_INTENTIFIER = 0x41;
	private static final int OFF_MAP_DESCRIPTION = 0x49; // 0x20 padded

	private static final int OFF_HEADS2 = 0x5d;
	private static final int OFF_SECTORS2 = 0x5f;

	private static final int OFF_BLOCK_SIZE_EXPONENT1 = 0x61;
	private static final int OFF_BLOCK_SIZE_EXPONENT2 = 0x62;
	private static final int OFF_BLOCK_SIZE = 0x63;

	//	private static final int OFF_UKN_3 = 0x63;

	private static final int OFF_MAP_NAME_CONT = 0x65;

	// 'Partition table' offsets.
	private static final int OFF_START_HEAD = 0x1bf;
	private static final int OFF_START_SECTOR = 0x1c0;
	private static final int OFF_START_CYLINDER = 0x1c1;
	private static final int OFF_SYSTEM_TYPE = 0x1c2;
	private static final int OFF_END_HEAD = 0x1c3;
	private static final int OFF_END_SECTOR = 0x1c4;
	private static final int OFF_END_CYLINDER = 0x1c5;
	private static final int OFF_REL_SECTORS = 0x1c6;
	private static final int OFF_NUMBER_OF_SECTORS = 0x1ca;
	private static final int OFF_PARTITION_SIG = 0x1fe;

	// Lengths of some of the fields
	private static final int LEN_MAP_NAME_CONT = 30;
	private static final int LEN_MAP_DESCRIPTION = 20;

	private FileSystemParam fsParams;

	private final ByteBuffer header = ByteBuffer.allocate(512);

	private ImgChannel file;
	private Date creationTime;

	private int sectorsPerTrack;
	private int headsPerCylinder;

	// Signatures.
	private static final byte[] FILE_ID = {
			'G', 'A', 'R', 'M', 'I', 'N', '\0'};

	private static final byte[] SIGNATURE = {
			'D', 'S', 'K', 'I', 'M', 'G', '\0'};

	private int numBlocks;

	ImgHeader(ImgChannel chan) {
		this.file = chan;
		header.order(ByteOrder.LITTLE_ENDIAN);
	}

	/**
	 * Create a header from scratch.
	 * @param params File system parameters.
	 */
	void createHeader(FileSystemParam params) {
		this.fsParams = params;

		header.put(OFF_XOR, (byte) 0);

		// Set the block size.  2^(E1+E2) where E1 is always 9.
		int exp = 9;

		int bs = params.getBlockSize();
		for (int i = 0; i < 32; i++) {
			bs >>>= 1;
			if (bs == 0) {
				exp = i;
				break;
			}
		}

		if (exp < 9)
			throw new IllegalArgumentException("block size too small");

		header.put(OFF_BLOCK_SIZE_EXPONENT1, (byte) 0x9);
		header.put(OFF_BLOCK_SIZE_EXPONENT2, (byte) (exp - 9));

		header.position(OFF_SIGNATURE);
		header.put(SIGNATURE);

		header.position(OFF_MAP_FILE_INTENTIFIER);
		header.put(FILE_ID);

		header.put(OFF_UNK_1, (byte) 0x2);

		// Actually this may not be the directory start block, I am guessing -
		// always assume it is 2 anyway.
		header.put(OFF_DIRECTORY_START_BLOCK, (byte) fsParams.getDirectoryStartEntry());

		header.position(OFF_CREATION_DATE);
		Utils.setCreationTime(header, creationTime);

		setDirectoryStartEntry(params.getDirectoryStartEntry());

		// Set the times.
		Date date = new Date();
		setCreationTime(date);
		setUpdateTime(date);
		setDescription(params.getMapDescription());
		if (fsParams.isGmapsupp()) {
			header.put(OFF_SUPP, (byte) (fsParams.isHideGmapsuppOnPC() ? 1: 0));
			int prodVersion = fsParams.getProductVersion();
			if (prodVersion >= 0) {
				// value 100 means 1.00 */
				int major = prodVersion / 100;
				int minor = prodVersion % 100;
				short version = (short) (major | (minor << 8)); 
				header.putShort(OFF_VERSION, version);
			}
		}
		// Checksum is not checked.
		header.put(OFF_CHECKSUM, (byte) 0);
	}

	/**
	 * Write out the values associated with the partition sizes.
	 *
	 * @param blockSize Block size.
	 */
	private void writeSizeValues(int blockSize) {
		int endSector = (int) (((numBlocks+1L) * blockSize + 511) / 512);
		//System.out.printf("end sector %d %x\n", endSector, endSector);

		// We have three maximum values for sectors, heads and cylinders.  We attempt to find values
		// for them that are larger than the 
		sectorsPerTrack = 32;   // 6 bit value
		headsPerCylinder = 128;
		int cyls = 0x400;

		// Try out various values of h, s and c until we find a combination that is large enough.
		// I'm not entirely sure about the valid values, but it seems that only certain values work
		// which is why we use values from a list.
		// See: http://www.win.tue.nl/~aeb/partitions/partition_types-2.html for justification for the h list
		out:
		for (int h : asList(16, 32, 64, 128, 256)) {
			for (int s : asList(4, 8, 16, 32)) {
				for (int c : asList(0x20, 0x40, 0x80, 0x100, 0x200, 0x3ff)) {
					log.info("shc=", s + "," + h + "," + c, "end=", endSector);
					//System.out.println("shc=" + s + "," + h + "," + c + "end=" + endSector);
					if (s * h * c > endSector) {
						headsPerCylinder = h;
						sectorsPerTrack = s;
						cyls = c;
						break out;
					}
				}
			}
		}

		// This sectors, head, cylinders stuff appears to be used by mapsource
		// and they have to be larger than the actual size of the map.  It
		// doesn't appear to have any effect on a garmin device or other software.
		header.putShort(OFF_SECTORS, (short) sectorsPerTrack);
		header.putShort(OFF_SECTORS2, (short) sectorsPerTrack);
		header.putShort(OFF_HEADS, (short) headsPerCylinder);
		header.putShort(OFF_HEADS2, (short) headsPerCylinder);
		header.putShort(OFF_CYLINDERS, (short) cyls);

		// Since there are only 2 bytes here it can overflow, if it
		// does we replace it with 0xffff.
		int blocks = (int) (endSector * 512L / blockSize);
		char shortBlocks = blocks > 0xffff ? 0xffff : (char) blocks;
		header.putChar(OFF_BLOCK_SIZE, shortBlocks);

		header.put(OFF_PARTITION_SIG, (byte) 0x55);
		header.put(OFF_PARTITION_SIG + 1, (byte) 0xaa);

		// Partition starts at zero. This is 0,0,1 in CHS terms.
		header.put(OFF_START_HEAD, (byte) 0);
		header.put(OFF_START_SECTOR, (byte) 1);
		header.put(OFF_START_CYLINDER, (byte) 0);

		header.put(OFF_SYSTEM_TYPE, (byte) 0);

		// Now calculate the CHS address of the last sector of the partition.
		CHS chs = new CHS(endSector - 1);

		header.put(OFF_END_HEAD, (byte) (chs.h));
		header.put(OFF_END_SECTOR, (byte) ((chs.s) | ((chs.c >> 2) & 0xc0)));
		header.put(OFF_END_CYLINDER, (byte) (chs.c & 0xff));

		// Write the LBA block address of the beginning and end of the partition.
		header.putInt(OFF_REL_SECTORS, 0);
		header.putInt(OFF_NUMBER_OF_SECTORS, endSector);
		log.info("number of blocks", endSector - 1);
	}

	void setHeader(ByteBuffer buf)  {
		buf.flip();
		header.put(buf);

		byte exp1 = header.get(OFF_BLOCK_SIZE_EXPONENT1);
		byte exp2 = header.get(OFF_BLOCK_SIZE_EXPONENT2);
		log.debug("header exponent", exp1, exp2);

		fsParams = new FileSystemParam();
		fsParams.setBlockSize(1 << (exp1 + exp2));
		fsParams.setDirectoryStartEntry(header.get(OFF_DIRECTORY_START_BLOCK));

		StringBuffer sb = new StringBuffer();
		sb.append(Utils.bytesToString(buf, OFF_MAP_DESCRIPTION, LEN_MAP_DESCRIPTION));
		sb.append(Utils.bytesToString(buf, OFF_MAP_NAME_CONT, LEN_MAP_NAME_CONT));

		fsParams.setMapDescription(sb.toString().trim());

		byte h = header.get(OFF_END_HEAD);
		byte sc1 = header.get(OFF_END_SECTOR);
		byte sc2 = header.get(OFF_END_CYLINDER);
		CHS chs = new CHS();
		chs.setFromPartition(h, sc1, sc2);
		int lba = chs.toLba();
		log.info("partition sectors", lba);
		// ... more to do
	}

	void setFile(ImgChannel file) {
		this.file = file;
	}
	
	FileSystemParam getParams() {
		return fsParams;
	}

	/**
	 * Sync the header to disk.
	 * @throws IOException If an error occurs during writing.
	 */
	public void sync() throws IOException {
		setUpdateTime(new Date());

		writeSizeValues(fsParams.getBlockSize());
		
		header.rewind();
		file.position(0);
		file.write(header);
		file.position(fsParams.getDirectoryStartEntry() * 512L);
	}

	/**
	 * Set the update time.
	 * @param date The date to use.
	 */
	protected void setUpdateTime(Date date) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(date);

		header.put(OFF_UPDATE_YEAR, toYearCode(cal.get(Calendar.YEAR)));
		header.put(OFF_UPDATE_MONTH, (byte) (cal.get(Calendar.MONTH)+1));
	}

	/**
	 * Set the description.  It is spread across two areas in the header.
	 *
	 * It appears that the description has to be in ascii.
	 *
	 * @param desc The description.
	 */
	protected void setDescription(String desc) {
		// Force the description to be in ascii.
		CodeFunctions funcs = CodeFunctions.createEncoderForLBL(0, 0);
		CharacterEncoder encoder = funcs.getEncoder();
		EncodedText enc = encoder.encodeText(desc);

		byte[] ctext = enc.getCtext();
		int len = enc.getLength() - 1;
		if (len > 50)
			throw new IllegalArgumentException("Description is too long (max 50)");

		byte[] part1 = new byte[LEN_MAP_DESCRIPTION];
		Arrays.fill(part1, (byte) ' ');

		byte[] part2 = new byte[LEN_MAP_NAME_CONT];
		Arrays.fill(part2, (byte) ' ');

		if (ctext != null) {
			if (len > LEN_MAP_DESCRIPTION) {
				System.arraycopy(ctext, 0, part1, 0, LEN_MAP_DESCRIPTION);
				System.arraycopy(ctext, LEN_MAP_DESCRIPTION, part2, 0, len - LEN_MAP_DESCRIPTION);
			} else {
				System.arraycopy(ctext, 0, part1, 0, len);
			}
		}

		header.position(OFF_MAP_DESCRIPTION);
		header.put(part1);

		header.position(OFF_MAP_NAME_CONT);
		header.put(part2);

		header.put((byte) 0); // really?
	}

	/**
	 * Convert a string to a byte array.
	 * @param s The string
	 * @return A byte array.
	 */
	private static byte[] toByte(String s) {
		// NB: what character set should be used?
		return s.getBytes();
	}

	/**
	 * Convert to the one byte code that is used for the year.
	 * If the year is in the 1900, then subtract 1900 and add the result to 0x63,
	 * else subtract 2000.
	 * Actually looks simpler, just subtract 1900..
	 * @param y The year in real-world format eg 2006.
	 * @return A one byte code representing the year.
	 */
	private static byte toYearCode(int y) {
		return (byte) (y - 1900);
	}

	protected void setDirectoryStartEntry(int directoryStartEntry) {
		header.put(OFF_DIRECTORY_START_BLOCK, (byte) directoryStartEntry);
		fsParams.setDirectoryStartEntry(directoryStartEntry);
	}

	protected void setCreationTime(Date date) {
		this.creationTime = date;
	}

	public void setNumBlocks(int numBlocks) {
		this.numBlocks = numBlocks;
	}

	public void hideGmapsuppOnPC (boolean b) {
		header.put(OFF_SUPP, (byte) (fsParams.isGmapsupp() && b ? 1: 0));
	}
	
	/**
	 * Represent a block number in the chs format.
	 *
	 * Note that this class uses the headsPerCylinder and sectorsPerTrack values
	 * from the enclosing class.
	 *
	 * @see <a href="http://en.wikipedia.org/wiki/Logical_Block_Addressing">Logical block addressing</a>
	 */
	private class CHS {
		private int h;
		private int s;
		private int c;

		private CHS() {
		}

		public CHS(int lba) {
			toChs(lba);
		}

		/**
		 * Calculate the CHS values from the the given logical block address.
		 * @param lba Input logical block address.
		 */
		private void toChs(int lba) {
			h = (lba / sectorsPerTrack) % headsPerCylinder;
			s = (lba % sectorsPerTrack) + 1;
			c = lba / (sectorsPerTrack * headsPerCylinder);
		}

		/**
		 * Set from a partition table entry.
		 *
		 * The cylinder is 10 bits and is split between the top 2 bit of the sector
		 * value and its own byte.
		 *
		 * @param h The h value.
		 * @param sc1 The s value (6 bits) and top 2 bits of c.
		 * @param sc2 The bottom 8 bits of c.
		 */
		public void setFromPartition(byte h, byte sc1, byte sc2) {
			this.h = h;
			this.s = (sc1 & 0x3f) + ((sc2 >> 2) & 0xc0);
			this.c = sc2 & 0xff;
		}

		public int toLba() {
			return (c * headsPerCylinder + h) * sectorsPerTrack + (s - 1);
		}
	}
}
