package backend;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Parses and stores iTunes library from XML file to a map.
 * @author Pyry Kröger
 *
 */
public class ItunesLibrary {
	
	private static final String KEY_ELEMENT = "key";
	private static final String ID_ELEMENT = "integer";
	private static final String TRACK_ID_VALUE = "Track ID";
	private static final String TRACK_LOC_VALUE = "Location";
	private static final String PLAYLIST_NAME_VALUE = "Name";
	private static final String TRACK_SIZE_VALUE = "Size";
	
	private Map<Integer,Song> tracks;
	private Map<String,Playlist> playlists;
	private String filename;
	private boolean wholeLibrarySync;
	private boolean fileAnalyzed;
	private ActionListener analysisReadyListener;
	private LibSync libsync;
	private boolean libfilenotfound;
	
	public ItunesLibrary(String filename, LibSync sync) {
		this.libsync = sync;
		this.filename = filename;
		this.fileAnalyzed = false;
		this.tracks = Collections.synchronizedMap(new HashMap<Integer,Song>());
		this.playlists = Collections.synchronizedMap(new TreeMap<String,Playlist>());

		if (filename != null) {
			Thread r = new Thread() {
				public void run() {
					try {
						Debug.println("Creating a new thread to analyze library "+ItunesLibrary.this.filename);
						ItunesLibrary.this.parseLibraryFile();
					} catch (IOException e) {
						//System.out.println("404!");
						ItunesLibrary.this.libsync.libNotFound(ItunesLibrary.this);
						ItunesLibrary.this.libfilenotfound = true;
						ItunesLibrary.this.fileAnalyzed = true;
						//System.out.println("This: "+ItunesLibrary.this.newLibFileToBeCreated());
					} catch (SAXParseException e) {
						ItunesLibrary.this.libsync.libNotFound(ItunesLibrary.this);
						ItunesLibrary.this.libfilenotfound = true;
						ItunesLibrary.this.fileAnalyzed = true;
					}
				}
			};
			r.start();
		}
		this.wholeLibrarySync = true;
		
	}
	
	public void setWholeLibrarySync(boolean flag) {
		this.wholeLibrarySync = flag;
	}
	
	public void forceFileAnalyzed(boolean flag) {
		this.fileAnalyzed = flag;
	}
	
	public boolean newLibFileToBeCreated() {
		return this.libfilenotfound;
	}
	
	public boolean wholeLibrarySync() {
		return this.wholeLibrarySync;
	}
	
	public void setAnalysisCompleteListener(ActionListener al) {
		this.analysisReadyListener = al;
		if (this.fileAnalyzed) {
			al.actionPerformed(null);
		}
	}
	
	private void parseLibraryFile() throws IOException,SAXParseException {
		SAXParserFactory spf = SAXParserFactory.newInstance();
		SAXParser p;
		try {
			p = spf.newSAXParser();
			File f = new File(this.filename);
			if (f.exists()) {
				Debug.println("Parsing "+this.filename+"...");
				p.parse(f, new LibraryParser());
				Debug.println(this.filename+" ready");
				this.fileAnalyzed = true;
				if (this.analysisReadyListener != null) {
					this.analysisReadyListener.actionPerformed(null);
				}
			} else {
				Debug.println("File "+this.filename+" not found!");
				this.fileAnalyzed = true;
				throw new FileNotFoundException("Library file " + this.filename + " not found.");
			}
		} catch (SAXParseException e) {
			throw new SAXParseException("Specified library file is invalid.",null);
		}
		catch (ParserConfigurationException e1) {
			e1.printStackTrace();
		} catch (SAXException e1) {
			e1.printStackTrace();
		}
	}
	
	public boolean fileAnalyzed() {
		return this.fileAnalyzed;
	}
	
	public void createPlaylist(String name) {
		this.playlists.put(name, new Playlist(name));
	}
	
	public ActionListener getActionListener() {
		return this.analysisReadyListener;
	}
	
	public void addToPlaylist(String playlistName, int songId) {
		//Debug.println("Adding song "+ songId + " to playlist "+playlistName);
		this.playlists.get(playlistName).addSong(songId);
	}
	
	public List<Integer> getSongsInPlaylist(String playlistName) {
		return this.playlists.get(playlistName).getSongs();
	}
	
	public void setPlaylistToBeSynced(String name, boolean flag) {
		Debug.println("Playlist "+name+" set to be "+(flag ? "" : "not ")+ "synced");
		this.playlists.get(name).toBeSynced(flag);
	}
	
	public Set<String> getPlaylists() {
		return this.playlists.keySet();
	}
	
	public Map<Integer,Song> getTracks() {
		return this.tracks;
	}
	
	public Map<Integer,Song> getSelectedTracks() {
		Map<Integer,Song> sTracks = new HashMap<Integer,Song>();
		for (String s : this.playlists.keySet()) {
			if (this.playlists.get(s).toBeSynced() || this.wholeLibrarySync) {
				for (int id : this.playlists.get(s).getSongs()) {
					sTracks.put(id, this.tracks.get(id));
				}
			}
		}
		return sTracks;
	}
	
	public long getSelectedTracksSize() {
		Map<Integer,Song> selectedTracks = this.getSelectedTracks();
		long size = 0;
		for (Integer s : selectedTracks.keySet()) {
			size += selectedTracks.get(s).getSize();
		}
		return size;
	}
	
	public String generateXMLPlaylist() {
		LibraryWriter output = new LibraryWriter();
		return output.generateLibrary();
	}
	
	public Map<String,String> generateM3UPlaylists() {
		HashMap<String,String> playlists = new HashMap<String,String>();
		for (String playlist : this.playlists.keySet()) {
			if (this.playlists.get(playlist).toBeSynced()) {
				playlists.put(playlist,"");
				for (int trId : this.playlists.get(playlist).getSongs()) {
					if (this.tracks.get(trId) != null) {
						String track = ItunesLibrary.getFilename(this.tracks.get(trId).getLocation());
					
					try {
						track = URLDecoder.decode(track, "UTF-8");
					} catch (UnsupportedEncodingException e) {
						e.printStackTrace();
					}
					track = track.substring(1); // remove preceding /
					playlists.put(playlist, playlists.get(playlist)+
							track+"\n");
					}
				}
			}
		}
		return playlists;
	}
	
	public static String getFilename(String fullpath) {
		String sep = File.separator;
		if (fullpath.lastIndexOf(sep) != -1) {
			return fullpath.substring(fullpath.lastIndexOf(sep));
		}
		return fullpath;
	}
	
	private class LibraryParser extends DefaultHandler {
		
		private String curElement;
		private boolean storeNextId;
		private boolean storeNextLoc;
		private boolean locStoringInProgress;
		private Integer curSongId;
		private boolean playlistScanInProgress;
		private boolean storeNextName;
		private String curPlaylist;
		private boolean nameStoringInProgress;
		private boolean storeNextSize;
		private boolean sizeStoringInProgress;
		private long curSize;
		private String curLoc = "";
		
		public void startElement(String uri, String localname,
	            String qName, Attributes atts) {
			this.curElement = qName;
			if (this.storeNextLoc) {
				this.locStoringInProgress = true;
			}
			if (this.storeNextName) {
				this.nameStoringInProgress = true;
				this.curPlaylist = "";
			} 
			if (this.storeNextSize) {
				this.sizeStoringInProgress = true;
				this.storeNextSize = false;
			}
			//System.out.println(qName);
		}
		
		public void characters(char[] ch, int start, int length) {
			
			/*
			 * This is a bit kludgy but works. Whenever parser encounters a
			 * <key> element with value "Track ID", it stores the value of
			 * next (<integer>) element as it is the actual track id. Similarly,
			 * whenever parser encounters <key> with value "Location", it stores
			 * next element's value to a map with previously encountered track id
			 * as a key.
			 * 
			 */
			
			String text = new String(ch,start,length);
			
			if (this.playlistScanInProgress) {
				if (this.curElement.equals(ItunesLibrary.KEY_ELEMENT) && 
						text.equals(ItunesLibrary.PLAYLIST_NAME_VALUE)) {
					this.storeNextName = true;
				} else if (this.nameStoringInProgress) {
					this.curPlaylist += text;
					this.storeNextName = false;
				}
				else if (this.curElement.equals(ItunesLibrary.KEY_ELEMENT) && 
						text.equals(ItunesLibrary.TRACK_ID_VALUE)) {
					this.storeNextId = true;
				} else if (this.storeNextId && this.curElement.equals(ItunesLibrary.ID_ELEMENT)) {
					ItunesLibrary.this.addToPlaylist(this.curPlaylist, Integer.parseInt(text));
					this.storeNextId = false;
				}
			} else {
				if (this.curElement.equals(ItunesLibrary.KEY_ELEMENT) && 
						text.equals(ItunesLibrary.TRACK_ID_VALUE)) {
					//System.out.println("Track ID: ");
					this.storeNextId = true;
				}
				else if (this.curElement.equals(ItunesLibrary.ID_ELEMENT) && this.storeNextId) {
					
					Integer id = Integer.parseInt(text);
					//System.out.println(id);
					this.curSongId = id;

					this.storeNextId = false;
				} 
				else if (text.equals(ItunesLibrary.TRACK_LOC_VALUE)) {
					this.storeNextLoc = true;
				}
				else if (this.locStoringInProgress) {
					this.curLoc = this.curLoc + text;
					this.storeNextLoc = false;
				} else if (this.curElement.equals(ItunesLibrary.KEY_ELEMENT) && 
						text.equals("Playlists")) {
					this.playlistScanInProgress = true;
				} else if (this.curElement.equals(ItunesLibrary.KEY_ELEMENT) && 
						text.equals(ItunesLibrary.TRACK_SIZE_VALUE)) {
					this.storeNextSize = true;
				} else if (this.sizeStoringInProgress) {
					this.curSize = Long.parseLong(text);
				}
			}
		}
		
		public void endElement(String uri, String localName, String qName) {
			if (this.locStoringInProgress) {
				this.locStoringInProgress = false;
				try {
					ItunesLibrary.this.tracks.put(Integer.valueOf(this.curSongId), new Song(this.curSongId,this.curLoc,this.curSize));
				} catch (URISyntaxException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IllegalArgumentException e) {
					System.out.println("Location: "+this.curLoc);
					e.printStackTrace();
				}
				this.curLoc = "";
			}
			if (this.nameStoringInProgress) {
				this.nameStoringInProgress = false;
				ItunesLibrary.this.createPlaylist(this.curPlaylist);
			}
			else if (this.curElement.equals("integer") && this.sizeStoringInProgress) {
				this.sizeStoringInProgress = false;
				
			}
		}
	}
	/**
	 * Does not create valid iTunes playlist file but more stripped-down version
	 * which is enough for syncing and does not consume as much disk space. 
	 * Example structure:
	 * <pre>
	 * &lt;plist version="1.0"&gt;
	 * &lt;key&gt;Tracks&lt;/key&gt;
	 * &lt;dict&gt;
	 * &lt;key&gt;123&lt;/key&gt;
	 * &lt;dict&gt;
	 * &lt;key&gt;Track ID&lt;/key&gt;&lt;integer&gt;123&lt;/integer&gt;
	 * &lt;key&gt;Size&lt;/key&gt;&lt;integer&gt;1234567&lt;/integer&gt;
	 * &lt;key&gt;Location&lt;/key&gt;&lt;string&gt;file://localhost/Music/Song.mp3&lt;/string&gt;
	 * &lt;/dict&gt;
	 * &lt;/dict&gt;
	 * &lt;/plist&gt;
	 * </pre>
	 * @author Pyry Kröger
	 *
	 */
	private class LibraryWriter {
		private DocumentBuilder builder;
		
		public LibraryWriter() {
		     DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		     try {
		    	this.builder = factory.newDocumentBuilder();
			} catch (ParserConfigurationException e) {
				e.printStackTrace();
			}
		}
		
		public String generateLibrary() {
			
			// generate dom model
			Document doc = this.builder.newDocument();
			Element plist = doc.createElement("plist");
			plist.setAttribute("version", "1.0");
			Element tracksKey = doc.createElement("key");
			tracksKey.appendChild(doc.createTextNode("Tracks"));
			plist.appendChild(tracksKey);
			Element tracklist = doc.createElement("dict");
			for (int id : ItunesLibrary.this.getSelectedTracks().keySet()) {
				Element trackIdKey = doc.createElement("key");
				trackIdKey.appendChild(doc.createTextNode(""+id));
				tracklist.appendChild(trackIdKey);
				Element trackDict = doc.createElement("dict");
				
				// track id
				Element trackDictIdKey = doc.createElement("key");
				trackDictIdKey.appendChild(doc.createTextNode("Track ID"));
				Element trackDictIdVal = doc.createElement("integer");
				trackDictIdVal.appendChild(doc.createTextNode(""+id));
				trackDict.appendChild(trackDictIdKey);
				trackDict.appendChild(trackDictIdVal);
				
				//track size
				Element trackDictSizeKey = doc.createElement("key");
				trackDictSizeKey.appendChild(doc.createTextNode("Size"));
				Element trackDictSizeVal = doc.createElement("integer");
				trackDictSizeVal.appendChild(doc.createTextNode(""+ItunesLibrary.this.getSelectedTracks().get(id).getSize()));
				trackDict.appendChild(trackDictSizeKey);
				trackDict.appendChild(trackDictSizeVal);
				
				// track location
				Element trackDictLocKey = doc.createElement("key");
				trackDictLocKey.appendChild(doc.createTextNode("Location"));
				Element trackDictLocVal = doc.createElement("string");
				trackDictLocVal.appendChild(doc.createTextNode(ItunesLibrary.this.getSelectedTracks().get(id).getEncodedLocation()));
				trackDict.appendChild(trackDictLocKey);
				trackDict.appendChild(trackDictLocVal);
				
				tracklist.appendChild(trackDict);
			}
			
			plist.appendChild(tracklist);
			
			// playlist information
			Element playlistsKey = doc.createElement("key");
			playlistsKey.appendChild(doc.createTextNode("Playlists"));
			plist.appendChild(playlistsKey);
			Element playlistArray = doc.createElement("array");
			for (String playlistname : ItunesLibrary.this.playlists.keySet()) {
				if (ItunesLibrary.this.playlists.get(playlistname) != null &&
						ItunesLibrary.this.playlists.get(playlistname).toBeSynced()) {  
				Element playlistsDict = doc.createElement("dict");
				Element playlistNameKey = doc.createElement("key");
				playlistNameKey.appendChild(doc.createTextNode("Name"));
				playlistsDict.appendChild(playlistNameKey);
				Element playlistName = doc.createElement("string");
				playlistName.appendChild(doc.createTextNode(playlistname));
				playlistsDict.appendChild(playlistName);
				Element playlistTrackArray = doc.createElement("array");
				for (int id : ItunesLibrary.this.playlists.get(playlistname).getSongs()) {
					Element playlistSongDict = doc.createElement("dict");
					Element playlistSongKey = doc.createElement("key");
					playlistSongKey.appendChild(doc.createTextNode("Track ID"));
					playlistSongDict.appendChild(playlistSongKey);
					
					Element playlistSongValue = doc.createElement("integer");
					playlistSongValue.appendChild(doc.createTextNode(id+""));
					playlistSongDict.appendChild(playlistSongValue);
					
					playlistTrackArray.appendChild(playlistSongDict);
				}
				playlistsDict.appendChild(playlistTrackArray);
				
				playlistArray.appendChild(playlistsDict);
				}
			}
			plist.appendChild(playlistArray);
			
			doc.appendChild(plist);

            //set up a transformer
            String xmlString;
			try {
				TransformerFactory transfac = TransformerFactory.newInstance();
				Transformer trans = transfac.newTransformer();
				trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
				trans.setOutputProperty(OutputKeys.INDENT, "yes");
				trans.setOutputProperty(OutputKeys.STANDALONE, "yes");

				//create string from xml tree
				StringWriter sw = new StringWriter();
				StreamResult result = new StreamResult(sw);
				DOMSource source = new DOMSource(doc);
				trans.transform(source, result);
				xmlString = sw.toString();
				
				//print xml
	            return xmlString;
	            
			} catch (TransformerConfigurationException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (TransformerFactoryConfigurationError e) {
				e.printStackTrace();
			} catch (TransformerException e) {
				e.printStackTrace();
			}
			
			return null;

		}
	}
}
