package org.flashtool.parsers.sin.v3;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Vector;

import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4FastDecompressor;

import org.flashtool.logger.LogProgress;
import org.flashtool.parsers.sin.v1.PartitionInfo;
import org.flashtool.system.OS;
import org.flashtool.util.BytesUtil;

import com.igormaznitsa.jbbp.JBBPParser;
import com.igormaznitsa.jbbp.io.JBBPBitInputStream;
import com.igormaznitsa.jbbp.io.JBBPByteOrder;
import com.igormaznitsa.jbbp.mapper.Bin;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SinParser {
	static byte[] hashv3len = {0, 0, 32};
	@Bin public byte[] magic;
	@Bin public int headerLen;
	@Bin public int payloadType;
	@Bin public int hashType;
	@Bin public int reserved;
	@Bin public int hashLen;
	public byte[] hashBlocks;
	public int certLen;
	public byte[] cert;
	//public byte[] dheader;
	public HashBlocks blocks;
	public DataHeader dataHeader;
	public Vector<Object> dataBlocks;
	public long size;
	private File sinfile;
	private long dataSize=0L;
	public byte[] partitioninfo = null;
	String dataType;
	
	public void setFile(File f) {
		sinfile=f;
	}
	
	  public void parseHash(JBBPBitInputStream sinStream) throws IOException {

		  hashBlocks = sinStream.readByteArray(hashLen);
		  certLen = sinStream.readInt(JBBPByteOrder.BIG_ENDIAN);
		  cert = sinStream.readByteArray(certLen);
		  
		  JBBPParser hashBlocksV3 = JBBPParser.prepare(
		            "blocks[_] {int length;"
	              + "byte["+hashv3len[hashType]+"] crc;}"
	      );
		  blocks = hashBlocksV3.parse(hashBlocks).mapTo(new org.flashtool.parsers.sin.v3.HashBlocks());

	  }
	  
	  public void parseDataHeader(JBBPBitInputStream sinStream) throws IOException {
			JBBPParser dataHeaderParser = JBBPParser.prepare(
					"byte[4] mmcfMagic;"
				  + "int mmcfLen;"
	              + "byte[4] gptpMagic;"
				  + "int gptpLen;"
	              + "byte[gptpLen-8] gptpuid;"
	        );

			JBBPParser addrBlocksParser = JBBPParser.prepare(
				    "int blockLen;"
				  + ">long dataOffset;"
	              + ">long dataLen;"
				  + ">long fileOffset;"
	              + "int hashType;"
	              + "byte[blockLen-36] checksum;"
	        );

			JBBPParser LZ4ABlocksParser = JBBPParser.prepare(
					  "int blockLen;"
					+ ">long dataOffset;"
				    + ">long uncompDataLen;"
					+ ">long compDataLen;"
				    + ">long fileOffset;"
					+ ">long reserved;"
					+ "int hashType;"
				    + "byte[blockLen-52] checksum;"
		        );

		    // First hash block seems to be Data header (addr map)
			//dheader = sinStream.readByteArray(blocks.blocks[0].length);
			//JBBPBitInputStream dheaderStream = new JBBPBitInputStream(new ByteArrayInputStream(dheader));
			try {
				byte[] mmcfmagic = new byte[4];
				sinStream.read(mmcfmagic);
				if (!new String(mmcfmagic).equals("MMCF")) throw new Exception("No MMCF");
				sinStream.skip(-4);
				dataHeader = dataHeaderParser.parse(sinStream).mapTo(new DataHeader());
			} catch (Exception e) {
				dataHeader = new DataHeader();
				dataHeader.mmcfMagic=new String("NULL").getBytes();
				dataHeader.gptpLen=0;
				dataHeader.mmcfLen=0;				
			}
			if (new String(dataHeader.mmcfMagic).equals("MMCF")) {
				long addrLength = dataHeader.mmcfLen-dataHeader.gptpLen;
				long read=0;
				byte[] amagic = new byte[4];
				dataBlocks = new Vector<Object>();
				while (read<addrLength) {
					sinStream.read(amagic);
					read+=4;
					if (new String(amagic).equals("ADDR")) {
						AddrBlock addrBlock = addrBlocksParser.parse(sinStream).mapTo(new AddrBlock());
						dataBlocks.add(addrBlock);
						read+=(addrBlock.blockLen-4);
					}
					if (new String(amagic).equals("LZ4A")) {
						LZ4ABlock lz4aBlock = LZ4ABlocksParser.parse(sinStream).mapTo(new LZ4ABlock());
						dataBlocks.add(lz4aBlock);
						read+=(lz4aBlock.blockLen-4);
					}
				}
			}
			else {
				dataHeader = new DataHeader();
				dataHeader.gptpLen=0;
				dataHeader.mmcfLen=0;
			}
			dataType=getDataTypePriv();
			dataSize = getDataSizePriv();
	  }
	  
	  public void setLength(long s) {
		  size=s;
	  }
	  
	  
	  public long getDataSizePriv() throws IOException {
		  if (dataSize>0) return dataSize;
		  if (dataHeader.mmcfLen>0) {
			  Object last = dataBlocks.lastElement();
			  if (last instanceof AddrBlock)
				  return ((AddrBlock)last).fileOffset+((AddrBlock)last).dataLen;
			  else
				  return ((LZ4ABlock)last).fileOffset+((LZ4ABlock)last).uncompDataLen;
		  }
		  else {
			  long size=0;
			  for (int i=0;i<this.blocks.blocks.length;i++) {
				  size+=this.blocks.blocks[i].length;
			  }
			  return size;
		  }		  
	  }

		public String getDataTypePriv(byte[] res) throws IOException {
			if (BytesUtil.startsWith(res, new byte[] {0x7F,0x45,0x4C,0x46})) return "elf";
			int pos = BytesUtil.indexOf(res, new byte[]{(byte)0x53,(byte)0xEF,(byte)0x01,(byte)0x00});
			if (pos==-1) return "unknown";
			pos = pos - 56;
			byte[] header = new byte[58];
			System.arraycopy(res, pos, header, 0, header.length);
			byte[] bcount = new byte[4];
			System.arraycopy(header, 4, bcount, 0, bcount.length);
			BytesUtil.revert(bcount);
			long blockcount = BytesUtil.getInt(bcount);
			dataSize = blockcount*4L*1024L;
			return "ext4";
		}

		public String getDataTypePriv() throws IOException {
			RandomAccessFile fin = new RandomAccessFile(sinfile,"r");
			byte[] res=null;
			byte[] rescomp=null;
			if (dataHeader.mmcfLen>0) {
				Object block = dataBlocks.firstElement();
				if (block instanceof AddrBlock) {
					res = new byte[(int)((AddrBlock)block).dataLen];
					fin.seek(getDataOffset()+((AddrBlock)block).dataOffset);
					fin.read(res);
					fin.close();
				}
				else {
					rescomp = new byte[(int)((LZ4ABlock)block).compDataLen];
					fin.seek(getDataOffset()+((LZ4ABlock)block).dataOffset);
					fin.read(rescomp);
					fin.close();
					LZ4Factory factory = LZ4Factory.fastestInstance();
					LZ4FastDecompressor decomp = factory.fastDecompressor();
					res = decomp.decompress(rescomp, (int)((LZ4ABlock)block).uncompDataLen);
				}
			}
			else {
				res = new byte[blocks.blocks[0].length];
				fin.seek(getDataOffset());
				fin.read(res);
				fin.close();
			}
			return getDataTypePriv(res);
		}
		
		public boolean hasPartitionInfo() {
			return false;
		}
		
		public byte[] getPartitionInfo() {
			return null;
		}
		
		public long getDataSize() {
			return dataSize;
		}
		
		public String getDataType() {
			return dataType;
		}

		public void dumpImage() throws IOException{
				RandomAccessFile fin = new RandomAccessFile(sinfile,"r");
				int count = 0;
				int bcount=0;
				if (dataHeader.mmcfLen>0)
					bcount = dataBlocks.size();
				else
					bcount = blocks.blocks.length;
				String ext = "."+dataType;
				String foutname = sinfile.getAbsolutePath().substring(0, sinfile.getAbsolutePath().length()-4)+ext;
				log.info("Generating empty container file");
				RandomAccessFile fout = OS.generateEmptyFile(foutname, dataSize, (byte)0xFF);
				if (fout!=null) {
					log.info("Container generated. Now extracting data to container");
					LogProgress.initProgress(bcount);
					long srcoffset=0;
					long destoffset=0;
					long dataLen=0;
					LZ4Factory factory = LZ4Factory.fastestInstance();
					LZ4FastDecompressor decomp = factory.fastDecompressor();
					long uncompLen=0;
					for (int i=0;i<bcount;i++) {
						if (dataHeader.mmcfLen>0) {
							Object objblock = dataBlocks.elementAt(i);
							if (objblock instanceof AddrBlock) {
								AddrBlock block = (AddrBlock)objblock;
								srcoffset=getDataOffset()+block.dataOffset;
								destoffset=block.fileOffset;
								dataLen=block.dataLen;
								uncompLen=0;
							}
							else {
								LZ4ABlock block = (LZ4ABlock)objblock;
								srcoffset=getDataOffset()+block.dataOffset;
								destoffset=block.fileOffset;
								dataLen=block.compDataLen;
								uncompLen=block.uncompDataLen;
							}
						}
						else {
							HashBlock block = blocks.blocks[i];
							if (i>0) srcoffset=srcoffset+blocks.blocks[i-1].length;
							else srcoffset=getDataOffset();
							destoffset=srcoffset-getDataOffset();
							dataLen=block.length;
						}
						fin.seek(srcoffset);
						fout.seek(destoffset);
						byte[] data = new byte[(int)dataLen];
						int nbread = fin.read(data);
						if (uncompLen>0) {
							byte[] res = factory.fastDecompressor().decompress(data,(int)uncompLen);
							fout.write(res);
						}
						else
							fout.write(data);
						LogProgress.updateProgress();
					}
				fout.close();
				fin.close();
				LogProgress.initProgress(0);
				log.info("Extraction finished to "+foutname);
			}
			else {
				log.error("An error occured while generating container");
			}
		}

		public void dumpHeader()  throws IOException {
			RandomAccessFile fin = new RandomAccessFile(sinfile,"r");
			String foutname = sinfile.getAbsolutePath().substring(0, sinfile.getAbsolutePath().length()-4)+".header";
			RandomAccessFile fout = new RandomAccessFile(foutname,"rw");
			byte[] buff = new byte[headerLen];
			fin.read(buff);
			fout.write(buff);
			fout.close();
			fin.close();
			log.info("Extraction finished to "+foutname);
		}

		public byte[] getHeader()  throws IOException {
			RandomAccessFile fin = new RandomAccessFile(sinfile,"r");
			byte[] buff = new byte[headerLen];
			fin.read(buff);
			fin.close();
			return buff;
		}

		public int getDataOffset() {
			if (dataHeader.mmcfLen>0)
				return headerLen+dataHeader.mmcfLen+8;
			else
				return headerLen;
		}

}
