package org.flashtool.flashsystem;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.Deflater;
import org.flashtool.gui.tools.FirmwareFileFilter;
import org.flashtool.gui.tools.XMLBootDelivery;
import org.flashtool.gui.tools.XMLPartitionDelivery;
import org.flashtool.logger.LogProgress;
import org.flashtool.parsers.sin.SinFile;
import org.flashtool.system.Devices;
import org.flashtool.system.OS;
import org.flashtool.system.XMLUpdate;
import com.turn.ttorrent.common.Torrent;
import lombok.extern.slf4j.Slf4j;


@Slf4j
public final class Bundle {

	private JarFile _firmware;
    private boolean _simulate=false;
    //private Properties bundleList=new Properties();
    private String _version;
    private String _branding;
    private String _device;
    private String _cda="";
    private String _revision="";
    private String _cmd25;
    private String _resetCust;
    private int maxbuffer;
    public final static int JARTYPE=1;
    public final static int FOLDERTYPE=2;
    private BundleMetaData _meta;
    private boolean bootdeliveryflashed=false;
    private XMLBootDelivery xmlb;
    private XMLPartitionDelivery xmlp;

    public String getJarName() {
    	return _firmware.getName();
    }
    
    public Bundle() {
    	_meta = new BundleMetaData();
    }
    
    public Bundle(String path, int type) throws Exception {
    	feed(path,type);
    }

    public void setMeta(BundleMetaData meta) throws Exception {
    	_meta = meta;
    	feedFromMeta();
    }
    
    public Bundle(String path, int type, BundleMetaData meta) throws Exception {
    	_meta = meta;
    	feed(path,type);
    }
    
    private void feed(String path, int type) throws Exception {
    	if (type==JARTYPE) feedFromJar(path);
    	if (type==FOLDERTYPE) feedFromFolder(path);    	
    }
    
	private void feedFromJar(String path) {
		try {
			_firmware = new JarFile(path);
			_meta = new BundleMetaData();
			_meta.setNoErase(_firmware.getManifest().getMainAttributes().getValue("noerase"));
			log.debug("Creating bundle from ftf file : "+_firmware.getName());
			_device = _firmware.getManifest().getMainAttributes().getValue("device");
			_version = _firmware.getManifest().getMainAttributes().getValue("version");
			_branding = _firmware.getManifest().getMainAttributes().getValue("branding");
			_cda = _firmware.getManifest().getMainAttributes().getValue("cda");
			_cmd25 = _firmware.getManifest().getMainAttributes().getValue("cmd25");
			Enumeration<JarEntry> e = _firmware.entries();
			while (e.hasMoreElements()) {
				BundleEntry entry = new BundleEntry(_firmware,e.nextElement());
				if (!entry.getName().toUpperCase().startsWith("BOOT/") && !entry.getName().toUpperCase().startsWith("PARTITION/")) {
				if (entry.getName().toUpperCase().endsWith("FSC") || entry.getName().toUpperCase().endsWith("SIN") || entry.getName().toUpperCase().endsWith("TA") || entry.getName().toUpperCase().endsWith("XML")) {
					try {
						_meta.process(entry);
					}
					catch (Exception e1) {e1.printStackTrace();
					}
					log.debug("Added this entry to the bundle list : "+entry.getName());
				}
				}
			}
		}
		catch (IOException ioe) {
			log.error("Cannot open the file "+path);
		}
	}

	private void feedFromFolder(String path) throws Exception {
		File[] list = (new File(path)).listFiles(new FirmwareFileFilter());
		for (int i=0;i<list.length;i++) {
			BundleEntry entry = new BundleEntry(list[i]);
			_meta.process(entry);
			log.debug("Added this entry to the bundle list : "+entry.getName());
		}
	}

	private void feedFromMeta() throws Exception {
		Iterator<Category> all = _meta.getAllEntries(true).iterator();
		while (all.hasNext()) {
			Category cat = all.next();
			Iterator<BundleEntry> icat = cat.getEntries().iterator();
			while (icat.hasNext()) {
				BundleEntry f = icat.next();
				_meta.process(f);
				log.debug("Added this entry to the bundle list : "+f.getName());
			}
		}
	}

	public void setLoader(File loader) {
		if (loader.exists()) {
			BundleEntry entry = new BundleEntry(loader);
			try {
				if (_meta!=null)
					_meta.setLoader(entry);
			}
			catch (Exception e) {
			}
		}
	}

	public void setFsc(File fsc) {
		BundleEntry entry = new BundleEntry(fsc);
		try {
			if (_meta!=null)
				_meta.setFsc(entry);
		}
		catch (Exception e) {
		}
	}

	public void setSimulate(boolean simulate) {
		_simulate = simulate;
	}

	public BundleEntry getEntry(String name) {
		Category c = _meta.get(Category.getCategoryFromName(name));
		return c.getEntries().iterator().next();
	}

	public BundleEntry searchEntry(String name) {
		Vector<BundleEntry> v = new Vector<BundleEntry>();
		Category c = _meta.get(Category.getCategoryFromName(name));
		if (c!=null && c.isEnabled()) return c.getEntries().iterator().next();
		return null;
	}

	public Enumeration <BundleEntry> allEntries() {
		Iterator<Category> icateg = _meta.getAllEntries(false).iterator();
		Vector<BundleEntry> v = new Vector<BundleEntry>();
		while (icateg.hasNext()) {
			Category c = icateg.next();
			Iterator<BundleEntry> ibentry = c.getEntries().iterator();
			while (ibentry.hasNext()) {
				v.add(ibentry.next());
			}
		}
		return v.elements();
	}
		
	public boolean isBootDeliveryFlashed() {
		return bootdeliveryflashed;
	}
	
	public void setBootDeliveryFlashed(boolean flashed) {
		bootdeliveryflashed=flashed;
	}

	public boolean hasLoader() {
		return _meta.getLoader()!=null;
	}

	public boolean hasFsc() {
		return _meta.getFsc()!=null;
	}

	public void setNoErase(String file) {
		try {
			XMLUpdate upd = new XMLUpdate(new File(file));
			_meta.setNoErase(upd.getNoErase());
		} catch (Exception e) {
		}
	}

	public BundleEntry getLoader() throws IOException, FileNotFoundException {
		if (hasLoader())
			return _meta.getLoader().getEntries().iterator().next();
		return null;
	}

	public BundleEntry getFsc() throws IOException, FileNotFoundException {
			return _meta.getFsc().getEntries().iterator().next();
	}

	public boolean hasBootDelivery() {
		Category bl = _meta.get("BOOT_DELIVERY");
		if (bl==null) return false;
		if (bl.isEnabled()) return true;
		return false;
	}

	public boolean hasPartitionDelivery() {
		Category bl = _meta.get("PARTITION_DELIVERY");
		if (bl==null) return false;
		if (bl.isEnabled()) return true;
		return false;
	}

	public BundleEntry getBootDelivery()  throws IOException, FileNotFoundException {
		if (hasBootDelivery())
			return _meta.get("BOOT_DELIVERY").getEntries().iterator().next();
		return null;
	}

	public BundleEntry getPartitionDelivery()  throws IOException, FileNotFoundException {
		if (hasPartitionDelivery())
			return _meta.get("PARTITION_DELIVERY").getEntries().iterator().next();
		return null;
	}

	public boolean simulate() {
		return _simulate;
	}
	
	public void setVersion(String version) {
		_version=version;
	}
	
	public void setBranding(String branding) {
		_branding=branding;
		
	}
	
	public void setDevice(String device) {
		_device=device;
	}
	
	public void setCDA(String cda) {
		_cda = cda;
	}

	public void setRevision(String revision) {
		_revision = revision;
	}
	
	public void setCmd25(String value) {
		_cmd25 = value;
		if (_cmd25==null) _cmd25="false";
	}
	
	public boolean hasCmd25() {
		try {
			return _cmd25.equals("true");
		}
		catch (Exception e) {
			return false;
		}
	}

	public void setResetStats(String value) {
		_resetCust = value;
		if (_resetCust==null) _resetCust="false";
	}

	public boolean hasResetStats() {
		try {
			return _resetCust.equals("true");
		}
		catch (Exception e) {
			return false;
		}
	}
	
	public void createFTF() throws Exception {
		File ftf = new File(OS.getFolderFirmwares()+File.separator+_device+"_"+_version+"_"+(_cda.length()>0?_cda:_branding)+(_revision.length()>0?("_"+_revision):"")+".ftf");
		if (ftf.exists()) throw new Exception("Bundle already exists");
		byte buffer[] = new byte[10240];
		StringBuffer sbuf = new StringBuffer();
		sbuf.append("Manifest-Version: 1.0\n");
		sbuf.append("Created-By: FlashTool\n");
		sbuf.append("version: "+_version+"\n");
		sbuf.append("branding: "+_branding+"\n");
		sbuf.append("cda: "+_cda+"\n");
		sbuf.append("revision: "+_revision+"\n");
		sbuf.append("device: "+_device+"\n");
		sbuf.append("cmd25: "+_cmd25+"\n");
		sbuf.append("noerase: "+this.getMeta().getNoEraseAsString()+"\n");
		Manifest manifest = new Manifest(new ByteArrayInputStream(sbuf.toString().getBytes("UTF-8")));
	    FileOutputStream stream = new FileOutputStream(ftf);
	    JarOutputStream out = new JarOutputStream(stream, manifest);
	    out.setLevel(Deflater.BEST_SPEED);
	    long size = 0L;
		Enumeration<BundleEntry> esize = allEntries();
		while (esize.hasMoreElements()) {
			size += esize.nextElement().getSize();
		}
		LogProgress.initProgress(size/10240+(size%10240>0?1:0));
		if (hasLoader()) {
			BundleEntry ldr = getLoader();
			log.info("Adding "+ldr.getName()+" to the bundle as "+ldr.getInternal());
		    JarEntry jarAddLdr = new JarEntry(ldr.getInternal());
	        out.putNextEntry(jarAddLdr);
	        InputStream inLdr = ldr.getInputStream();
	        while (true) {
	          int nRead = inLdr.read(buffer, 0, buffer.length);
	          if (nRead <= 0)
	            break;
	          out.write(buffer, 0, nRead);
	        }
	        inLdr.close();
		}
		if (hasFsc()) {
			BundleEntry fsc = getFsc();
			if (fsc!=null) {
				log.info("Adding "+fsc.getName()+" to the bundle as "+fsc.getInternal());
			    JarEntry jarAddFsc = new JarEntry(fsc.getInternal());
		        out.putNextEntry(jarAddFsc);
		        InputStream inFsc = fsc.getInputStream();
		        while (true) {
		          int nRead = inFsc.read(buffer, 0, buffer.length);
		          if (nRead <= 0)
		            break;
		          out.write(buffer, 0, nRead);
		        }
		        inFsc.close();
			}
		}
        Enumeration<BundleEntry> e = allEntries();
		while (e.hasMoreElements()) {
			BundleEntry entry = e.nextElement();
			log.info("Adding "+entry.getName()+" to the bundle as "+entry.getInternal());
		    JarEntry jarAdd = new JarEntry(entry.getInternal());
	        out.putNextEntry(jarAdd);
	        InputStream in = entry.getInputStream();
	        while (true) {
	          int nRead = in.read(buffer, 0, buffer.length);
	          if (nRead <= 0)
	            break;
	          out.write(buffer, 0, nRead);
	          LogProgress.updateProgress();
	        }
	        in.close();
	        if (new File(entry.getAbsolutePath()).getParentFile().getName().toUpperCase().equals("BOOT")) {
	        	String folder = new File(entry.getAbsolutePath()).getParentFile().getAbsolutePath();
				XMLBootDelivery xml = new XMLBootDelivery(new File(entry.getAbsolutePath()));
				Enumeration files = xml.getFiles();
				while (files.hasMoreElements()) {
					String bootname = (String)files.nextElement();
					log.info("Adding "+bootname+" to the bundle");
				    jarAdd = new JarEntry("boot/"+bootname);
			        out.putNextEntry(jarAdd);
			        InputStream bin = new FileInputStream(new File(folder+File.separator+bootname));
			        while (true) {
			          int nRead = bin.read(buffer, 0, buffer.length);
			          if (nRead <= 0)
			            break;
			          out.write(buffer, 0, nRead);
			          LogProgress.updateProgress();
			        }
			        bin.close();
				}

	        }
	        if (new File(entry.getAbsolutePath()).getParentFile().getName().toUpperCase().equals("PARTITION")) {
	        	String folder = new File(entry.getAbsolutePath()).getParentFile().getAbsolutePath();
				XMLPartitionDelivery xml = new XMLPartitionDelivery(new File(entry.getAbsolutePath()));
				Enumeration files = xml.getFiles();
				while (files.hasMoreElements()) {
					String partitionname = (String)files.nextElement();
					log.info("Adding "+partitionname+" to the bundle");
				    jarAdd = new JarEntry("partition/"+partitionname);
			        out.putNextEntry(jarAdd);
			        InputStream bin = new FileInputStream(new File(folder+File.separator+partitionname));
			        while (true) {
			          int nRead = bin.read(buffer, 0, buffer.length);
			          if (nRead <= 0)
			            break;
			          out.write(buffer, 0, nRead);
			          LogProgress.updateProgress();
			        }
			        bin.close();
				}

	        }
		}
		out.close();
	    stream.close();
	    log.info("Creating torrent file : "+ftf.getAbsolutePath()+".torrent");
	    List<URI> l1 = new ArrayList<URI>();
	    List<URI> l2 = new ArrayList<URI>();
	    List<URI> l3 = new ArrayList<URI>();
	    l1.add(new URI("udp://tracker.openbittorrent.com:80/announce"));
	    l2.add(new URI("udp://tracker.publicbt.com:80/announce"));
	    l3.add(new URI("udp://tracker.ccc.de:80/announce"));
	    List<List<URI>> parent = new ArrayList<List<URI>>();
	    parent.add(l1);
	    parent.add(l2);
	    parent.add(l3);
	    Torrent torrent = Torrent.create(ftf, null, new URI("udp://tracker.openbittorrent.com:80/announce"), "FlashTool");
	    FileOutputStream fout =new FileOutputStream(new File(ftf.getAbsolutePath()+".torrent")); 
	    torrent.save(fout);
	    fout.flush();
	    fout.close();
	    log.info("Torrent file creation finished");
	    LogProgress.initProgress(0);
	}
	
	public long getMaxLoaderProgress() {
		int maxdatasize=0;
		int maxloadersize=0;
		try {
			if (hasLoader()) {
				SinFile loader = new SinFile(new File(getLoader().getAbsolutePath()));
				if (loader.getVersion()>=2) {
					maxloadersize=0x10000;
				}
				else {
					maxloadersize=0x1000;
				}
			} else maxloadersize=0x1000;
		}
		catch (Exception e) {
			maxloadersize=0x1000;
		}
	    Iterator<Category> e = getMeta().getAllEntries(true).iterator();
	    long totalsize = 8;
	    while (e.hasNext()) {
	    	Category cat = e.next();
	    	Iterator<BundleEntry> icat = cat.getEntries().iterator();
	    	while (icat.hasNext()) {
	    	BundleEntry entry = getEntry(icat.next().getName());
	    	try {
	    		if (!entry.getName().toUpperCase().endsWith(".TA")) {
	    			long filecount = 0;
	    			SinFile s = null;
				    if (entry.getName().contains("loader")) {
			    		s = new SinFile(new File(entry.getAbsolutePath()));
				    	s.setChunkSize(maxloadersize);
		    			filecount++;
				    }
		    		filecount = filecount + s.getNbChunks()+1;
		    		totalsize += filecount;
	    		}
	    	} catch (Exception ex) {}
	    	}
	    }
	    return totalsize;
	}

	public long getMaxProgress(int chunksize) {
		    Iterator<Category> e = getMeta().getAllEntries(true).iterator();
		    long totalsize = 15;
		    while (e.hasNext()) {
		    	Category cat = e.next();
		    	Iterator<BundleEntry> icat = cat.getEntries().iterator();
		    	while (icat.hasNext()) {
			    	BundleEntry entry = icat.next();
			    	try {
			    		if (!entry.getName().toUpperCase().endsWith(".TA")) {
			    			if (!entry.getName().toUpperCase().contains("LOADER")) {
			    				if (entry.getName().toUpperCase().endsWith("SIN")) {
						    		long filecount = 0;
						    		SinFile s = new SinFile(new File(entry.getAbsolutePath()));
						    		s.setChunkSize(chunksize);
						    		filecount = filecount + s.getNbChunks()+1;
						    		totalsize += filecount;
			    				}
			    			}
			    		}
			    	} catch (Exception ex) {}
		    	}
		    }
		    if (hasCmd25()) totalsize = totalsize + 1;
		    return totalsize;
	}

	public boolean open() {
		try {
			log.info("Preparing files for flashing");
			
			OS.deleteDirectory(new File(OS.getFolderFirmwaresPrepared()));
			File f = new File(OS.getFolderFirmwaresPrepared());
			f.mkdir();
			log.debug("Created the "+f.getName()+" folder");
			LogProgress.initProgress(_meta.getAllEntries(true).size());
			Iterator<Category>  entries = _meta.getAllEntries(true).iterator();
			while (entries.hasNext()) {
				LogProgress.updateProgress();
				Category categ = entries.next();
				Iterator<BundleEntry> icateg = categ.getEntries().iterator();
				while (icateg.hasNext()) {
					BundleEntry bf = icateg.next();
					bf.saveTo(OS.getFolderFirmwaresPrepared());
					if (bf.getCategory().equals("BOOT_DELIVERY")) {
						xmlb = new XMLBootDelivery(new File(bf.getAbsolutePath()));
						Enumeration files = xmlb.getFiles();
						while (files.hasMoreElements()) {
							String file = (String)files.nextElement();
							try {
								JarEntry j = _firmware.getJarEntry("boot/"+file);
								BundleEntry bent = new BundleEntry(_firmware,j);
								bent.saveTo(OS.getFolderFirmwaresPrepared());
							} catch (NullPointerException npe) {
								JarEntry j = _firmware.getJarEntry("boot/"+file+"b");
								BundleEntry bent = new BundleEntry(_firmware,j);
								bent.saveTo(OS.getFolderFirmwaresPrepared());
							}
						}
					}
					if (bf.getCategory().equals("PARTITION_DELIVERY")) {
						xmlp = new XMLPartitionDelivery(new File(bf.getAbsolutePath()));
						Enumeration files = xmlp.getFiles();
						while (files.hasMoreElements()) {
							String file = (String)files.nextElement();
							JarEntry j = _firmware.getJarEntry("partition/"+file);
							BundleEntry bent = new BundleEntry(_firmware,j);
							bent.saveTo(OS.getFolderFirmwaresPrepared());
						}
					}
				}
			}
			if (hasLoader())
				getLoader().saveTo(OS.getFolderFirmwaresPrepared());
			if (hasFsc()) {
				getFsc().saveTo(OS.getFolderFirmwaresPrepared());
			}
			LogProgress.initProgress(0);
			return true;
		}
		catch (Exception e) {
			e.printStackTrace();
			log.error(e.getMessage());
			LogProgress.initProgress(0);
			return false;
		}
    }
	
	public void close() {
		if (_firmware !=null) {
			File f=null;
			Enumeration<JarEntry> e=_firmware.entries();
			while (e.hasMoreElements()) {
				JarEntry entry = e.nextElement();
				String outname = OS.getFolderFirmwaresPrepared()+File.separator+entry.getName();
				if (entry.getName().toUpperCase().endsWith(".SINB") || entry.getName().toUpperCase().endsWith(".TAB")) {
					outname = outname.replace(".sinb", ".sin").replace(".tab", ".ta");
				}
				f = new File(outname);
				if (f.exists())
					f.delete();
			}
			f = new File(OS.getFolderFirmwaresPrepared()+File.separator+"boot");
			if (f.exists())
				f.delete();
			f = new File(OS.getFolderFirmwaresPrepared());
			if (f.exists())
				f.delete();
			try {
				_firmware.close();
			}
			catch (IOException ioe) {ioe.printStackTrace();}
		}
	}
	
	public BundleMetaData getMeta() {
		return _meta;
	}

	public String getDevice() {
		return _device;
	}

	public String getVersion() {
		return _version;
	}
	
	public String toString() {
	    return "Bundle for " + Devices.getVariantName(_device) + ". FW release : " + _version + ". Customization : " + _branding;
	}
	
	public XMLBootDelivery getXMLBootDelivery() {
		return xmlb;
	}

	public XMLPartitionDelivery getXMLPartitionDelivery() {
		return xmlp;
	}
	
	public void setMaxBuffer(int value) {
		maxbuffer=value;
	}
	
	public int getMaxBuffer() {
		return maxbuffer;
	}

}