package app.musicplayer.util;

import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.audio.AudioHeader;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import app.musicplayer.MusicPlayer;
import app.musicplayer.model.Library;
import app.musicplayer.model.Song;

public class XMLEditor {
	
	private static String musicDirectory;
	
	// Initializes array lists to store the file names of the songs in the xml file.
	// This array lists will be checked to determine if a song has been added or deleted from the music directory.
	private static ArrayList<String> xmlSongsFileNames = new ArrayList<>();
	// Stores the file paths of the xml songs.
	// This is important if a song has to be removed from the xml file as it is used to find the node to remove. 
	private static ArrayList<String> xmlSongsFilePaths = new ArrayList<>();
	
	// Initializes array lists to store the filenames of the songs in the music directory.
	// This array lists will be checked to determine if a song has been added or deleted from the music directory.
	private static ArrayList<String> musicDirFileNames = new ArrayList<>();
	// Stores files in the music directory.
	// This is important if a song has to be added to the xml file and it is used to find the file to add.
	private static ArrayList<File> musicDirFiles = new ArrayList<>();
	
	// Initializes array list with song files of songs to be added to library.xml
	private static ArrayList<File> songFilesToAdd = new ArrayList<>();
	
	// Initializes array list with song paths of songs to be deleted from library.xml
	private static ArrayList<String> songPathsToDelete = new ArrayList<>();

	private static ArrayList<Song> songsToAdd = new ArrayList<>();
	
	// Initializes booleans used to determine how the library.xml file needs to be edited.
	private static boolean addSongs;
	private static boolean deleteSongs;

	public static ArrayList<Song> getNewSongs() { return songsToAdd; }

	public static void setMusicDirectory(Path musicDirectoryPath) {
		musicDirectory = musicDirectoryPath.toString();
	}

	public static void addDeleteChecker() {
		// Finds the file name of the songs in the library xml file and
		// stores them in the xmlSongsFileNames array list.
		xmlSongsFilePathFinder();

		// Finds the song titles in the music directory and stores them in the librarySongs array list.
		musicDirFileFinder(new File(musicDirectory));
							
		// Initializes a counter variable to index the musicDirFiles array to get the file
		// corresponding to the song that needs to be added to the xml file.
		int i = 0;
		// Loops through musicDirFiles and checks if the song file names are in the library.xml file. 
		// If not, then the song needs to be ADDED.
		for (String songFileName : musicDirFileNames) {
			// If the song file name is not in the xmlSongsFilenames,
			// then it was added to the music directory and needs to be added to the xml file.
			if (!xmlSongsFileNames.contains(songFileName)) {
				// Adds the song file that needs to be added to the array list in XMLEditor.
				songFilesToAdd.add(musicDirFiles.get(i));
				addSongs = true;
			}
			i++;
		}
		
		// Initializes a counter variable to index the xmlSongsFilePaths array to get the
		// file path of the songs that need to be removed from the xml file.
		int j = 0;
		// Loops through xmlSongsFileNames and checks if all the xml songs are in the music directory.
		// If one of the songs in the xml file is not in the music directory, then it was DELETED.
		for (String songFileName : xmlSongsFileNames) {
			// If the songFileName is not in the musicDirFileNames,
			// then it was deleted from the music directory and needs to be deleted from the xml file.
			if (!musicDirFileNames.contains(songFileName)) {
				// Adds the songs that needs to be deleted to the array list in XMLEditor.
				songPathsToDelete.add(xmlSongsFilePaths.get(j));
				deleteSongs = true;
			}
			j++;
		}
		
		// If a song needs to be added to the xml file.
		if (addSongs) {	
            // Adds the new song to the xml file.
			addSongToXML();
		}
		
        // If a song needs to be deleted from the xml file.
		if (deleteSongs) {
			// Deletes song from library xml file.
			deleteSongFromXML();
		}
		
	}
	
	private static void xmlSongsFilePathFinder() {
		try {
			// Creates reader for xml file.
			XMLInputFactory factory = XMLInputFactory.newInstance();
			factory.setProperty("javax.xml.stream.isCoalescing", true);
			FileInputStream is = new FileInputStream(new File(Resources.JAR + "library.xml"));
			XMLStreamReader reader = factory.createXMLStreamReader(is, "UTF-8");
			
			String element = null;
			String songLocation;
			
			// Loops through xml file looking for song titles.
			// Stores the song title in the xmlSongsFileNames array list.
			while(reader.hasNext()) {
			    reader.next();
			    if (reader.isWhiteSpace()) {
			        continue;
			    } else if (reader.isStartElement()) {
			    	element = reader.getName().getLocalPart();
			    } else if (reader.isCharacters() && element.equals("location")) {
			    	// Retrieves the song location and adds it to the corresponding array list.
			    	songLocation = reader.getText();
			    	xmlSongsFilePaths.add(songLocation);
			    	
			    	// Retrieves the file name from the file path and adds it to the xmlSongsFileNames array list.
			    	int i = songLocation.lastIndexOf("\\");
			    	String songFileName = songLocation.substring(i + 1, songLocation.length());
			    	xmlSongsFileNames.add(songFileName);
			    }
			}
			// Closes xml reader.
			reader.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private static void musicDirFileFinder(File musicDirectoryFile) {
    	// Lists all the files in the music directory and stores them in an array.
        File[] files = musicDirectoryFile.listFiles();

        // Loops through the files.
        for (File file : files) {
            if (file.isFile() && Library.isSupportedFileType(file.getName())) {
            	// Adds the file to the musicDirFiles array list. 
            	musicDirFiles.add(file);
            	
            	// Adds the file name to the musicDirFileNames array list.
            	musicDirFileNames.add(file.getName());
            } else if (file.isDirectory()) {
            	musicDirFileFinder(file);
            }
        }
	}
	
	private static void addSongToXML() {
		// Initializes the array list with song objects to add to the xml file.
		createNewSongObject();
		
		if (songsToAdd.size() == 0) {
			return;
		}
		
        try {
			DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
			Document doc = docBuilder.parse(Resources.JAR + "library.xml");
			
            XPathFactory xPathfactory = XPathFactory.newInstance();
            XPath xpath = xPathfactory.newXPath();
            
            // Creates node to add songs.
            XPathExpression expr = xpath.compile("/library/songs");
            Node songsNode = ((NodeList) expr.evaluate(doc, XPathConstants.NODESET)).item(0);
            
            // Loops through the songs in the new song array list and adds them to the xml file.
            for (Song song : songsToAdd) {
                // Creates a new song element and its sub elements.
                Element newSong = doc.createElement("song");
                Element newSongId = doc.createElement("id");
                Element newSongTitle = doc.createElement("title");
                Element newSongArtist = doc.createElement("artist");
                Element newSongAlbum = doc.createElement("album");
                Element newSongLength = doc.createElement("length");
                Element newSongTrackNumber = doc.createElement("trackNumber");
                Element newSongDiscNumber = doc.createElement("discNumber");
                Element newSongPlayCount = doc.createElement("playCount");
                Element newSongPlayDate = doc.createElement("playDate");
                Element newSongLocation = doc.createElement("location");

                // Saves the new song data.
                newSongId.setTextContent(Integer.toString(song.getId()));
                newSongTitle.setTextContent(song.getTitle());
                newSongArtist.setTextContent(song.getArtist());
                newSongAlbum.setTextContent(song.getAlbum());
                newSongLength.setTextContent(Long.toString(song.getLengthInSeconds()));
                newSongTrackNumber.setTextContent(Integer.toString(song.getTrackNumber()));
                newSongDiscNumber.setTextContent(Integer.toString(song.getDiscNumber()));
                newSongPlayCount.setTextContent(Integer.toString(song.getPlayCount()));
                newSongPlayDate.setTextContent(song.getPlayDate().toString());
                newSongLocation.setTextContent(song.getLocation());
                
                // Adds the new song to the xml file.
                songsNode.appendChild(newSong);
                // Adds the new song data to the new song.
                newSong.appendChild(newSongId);
                newSong.appendChild(newSongTitle);
                newSong.appendChild(newSongArtist);
                newSong.appendChild(newSongAlbum);
                newSong.appendChild(newSongLength);
                newSong.appendChild(newSongTrackNumber);
                newSong.appendChild(newSongDiscNumber);
                newSong.appendChild(newSongPlayCount);
                newSong.appendChild(newSongPlayDate);
                newSong.appendChild(newSongLocation);
            }
            
            // Calculates the new xml file number, taking into account the new songs.
            int newXMLFileNum = MusicPlayer.getXMLFileNum() + songFilesToAdd.size();

            // Creates node to update xml file number.
            expr = xpath.compile("/library/musicLibrary/fileNum");
            Node fileNum = ((NodeList) expr.evaluate(doc, XPathConstants.NODESET)).item(0);
            
            // Updates the fileNum field in the xml file.
            fileNum.setTextContent(Integer.toString(newXMLFileNum));
            // Updates the xmlFileNum in MusicPlayer. 
            MusicPlayer.setXMLFileNum(newXMLFileNum);
            
            // Gets the new last id assigned after adding all the new songs.
            int newLastIdAssigned = songsToAdd.get(songsToAdd.size() - 1).getId();
            
            // Creates node to update xml last id assigned.
            expr = xpath.compile("/library/musicLibrary/lastId");
            Node lastId = ((NodeList) expr.evaluate(doc, XPathConstants.NODESET)).item(0);
            
            // Updates the last id in the xml file.
            lastId.setTextContent(Integer.toString(newLastIdAssigned));
            // Updates the lastId in MusicPlayer.
        	MusicPlayer.setLastIdAssigned(newLastIdAssigned);
            
            
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            DOMSource source = new DOMSource(doc);
            File xmlFile = new File(Resources.JAR + "library.xml");
            StreamResult result = new StreamResult(xmlFile);
            transformer.transform(source, result);
            
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
	
	private static void createNewSongObject() {
		
		// Searches the xml file to get the last id assigned.
		int lastIdAssigned = xmlLastIdAssignedFinder();
		
		// Loops through each song file that needs to be added and creates a song object for each.
		// Each song object is added to an array list and returned so that they can be added to the xml file.
		for (File songFile : songFilesToAdd) {
	        try {
	            AudioFile audioFile = AudioFileIO.read(songFile);
	            Tag tag = audioFile.getTag();
	            AudioHeader header = audioFile.getAudioHeader();
	            
	            // Gets song properties.
	            int id = ++lastIdAssigned;
	            String title = tag.getFirst(FieldKey.TITLE);
	            // Gets the artist, empty string assigned if song has no artist.
	            String artistTitle = tag.getFirst(FieldKey.ALBUM_ARTIST);
	            if (artistTitle == null || artistTitle.equals("") || artistTitle.equals("null")) {
	                artistTitle = tag.getFirst(FieldKey.ARTIST);
	            }
	            String artist = (artistTitle == null || artistTitle.equals("") || artistTitle.equals("null")) ? "" : artistTitle;
	            String album = tag.getFirst(FieldKey.ALBUM);
	            // Gets the track length (as an int), converts to long and saves it as a duration object.                
	            Duration length = Duration.ofSeconds((long) header.getTrackLength());
	            // Gets the track number and converts to an int. Assigns 0 if a track number is null.
	            String track = tag.getFirst(FieldKey.TRACK);                
	            int trackNumber = Integer.parseInt((track == null || track.equals("") || track.equals("null")) ? "0" : track);
	            // Gets disc number and converts to int. Assigns 0 if the disc number is null.
	            String disc = tag.getFirst(FieldKey.DISC_NO);
	            int discNumber = Integer.parseInt((disc == null || disc.equals("") || disc.equals("null")) ? "0" : disc);
	            int playCount = 0;
	            LocalDateTime playDate = LocalDateTime.now();
	            String location = Paths.get(songFile.getAbsolutePath()).toString();
	            
	            // Creates a new song object for the added song and adds it to the newSongs array list.
	            Song newSong = new Song(id, title, artist, album, length, trackNumber, discNumber, playCount, playDate, location);

	            // Adds the new song to the songsToAdd array list.
	            songsToAdd.add(newSong);
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}
		// Updates the lastIdAssigned in MusicPlayer to account for the new songs.
		MusicPlayer.setLastIdAssigned(lastIdAssigned);
	}
	
    private static int xmlLastIdAssignedFinder() {
		try {
			// Creates reader for xml file.
			XMLInputFactory factory = XMLInputFactory.newInstance();
			factory.setProperty("javax.xml.stream.isCoalescing", true);
			FileInputStream is = new FileInputStream(new File(Resources.JAR + "library.xml"));
			XMLStreamReader reader = factory.createXMLStreamReader(is, "UTF-8");
			
			String element = null;
			String lastId = null;
			
			// Loops through xml file looking for the music directory file path.
			while(reader.hasNext()) {
			    reader.next();
			    if (reader.isWhiteSpace()) {
			        continue;
			    } else if (reader.isStartElement()) {
			    	element = reader.getName().getLocalPart();
			    } else if (reader.isCharacters() && element.equals("lastId")) {
			    	lastId = reader.getText();               	
			    	break;
			    }
			}
			// Closes xml reader.
			reader.close();
			
			// Converts the file number to an int and returns the value. 
			return Integer.parseInt(lastId);
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
    }
	
	private static void deleteSongFromXML() {
		// Gets the currentXMLFileNum.
		int currentXMLFileNum = MusicPlayer.getXMLFileNum();

        try {
			DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
			Document doc = docBuilder.parse(Resources.JAR + "library.xml");
			
            XPathFactory xPathfactory = XPathFactory.newInstance();
            XPath xpath = xPathfactory.newXPath();
            
            // Retrieves the last id assigned to a song from the xml file.
            int xmlLastIdAssigned = xmlLastIdAssignedFinder();

            // Finds the song node corresponding to the last assigned id.
            XPathExpression expr = xpath.compile("/library/songs/song[id/text() = \"" + xmlLastIdAssigned + "\"]");
            Node lastSongNode = ((NodeList) expr.evaluate(doc, XPathConstants.NODESET)).item(0);
            
            // Loops through the songPathsToDelete array list and removes the nodes from the xml file.
            Node deleteSongNode = null;
            for (String songFilePath : songPathsToDelete) {
                // Finds the node with the song title marked for removal.
            	expr = xpath.compile("/library/songs/song[location/text() = \"" + songFilePath + "\"]");
                deleteSongNode = ((NodeList) expr.evaluate(doc, XPathConstants.NODESET)).item(0);
                
                // Removes the node corresponding to the title of the song.
                deleteSongNode.getParentNode().removeChild(deleteSongNode);

            	// Decreases the counter for the number of files in the xml file.
                currentXMLFileNum--;
            }
            
            // If the last node to be deleted was the last song node,
            // then the new last assigned id is found and updated in the MusicPlayer and xml file.
            if (deleteSongNode == lastSongNode) {
            	int newLastIdAssigned = xmlNewLastIdAssignedFinder();

                // Creates node to update xml last id assigned.
                expr = xpath.compile("/library/musicLibrary/lastId");
                Node lastId = ((NodeList) expr.evaluate(doc, XPathConstants.NODESET)).item(0);
                
                // Updates the lastId in MusicPlayer and in the xml file.
            	MusicPlayer.setLastIdAssigned(newLastIdAssigned);
                lastId.setTextContent(Integer.toString(newLastIdAssigned));
            }
            
            // Creates node to update xml file number.
            XPathExpression fileNumExpr = xpath.compile("/library/musicLibrary/fileNum");
            Node fileNum = ((NodeList) fileNumExpr.evaluate(doc, XPathConstants.NODESET)).item(0);
            
            // Updates the fileNum in MusicPlayer and in the xml file.
            MusicPlayer.setXMLFileNum(currentXMLFileNum);
            fileNum.setTextContent(Integer.toString(currentXMLFileNum));
                    
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            DOMSource source = new DOMSource(doc);
            File xmlFile = new File(Resources.JAR + "library.xml");
            StreamResult result = new StreamResult(xmlFile);
            transformer.transform(source, result);
            
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
	
    private static int xmlNewLastIdAssignedFinder() {
		try {
			// Creates reader for xml file.
			XMLInputFactory factory = XMLInputFactory.newInstance();
			factory.setProperty("javax.xml.stream.isCoalescing", true);
			FileInputStream is = new FileInputStream(new File(Resources.JAR + "library.xml"));
			XMLStreamReader reader = factory.createXMLStreamReader(is, "UTF-8");
			
			String element = null;
			String location;
			
			String currentSongId = null;
			String xmlNewLastIdAssigned = null;
			
			// Loops through xml file looking for the music directory file path.
			while(reader.hasNext()) {
			    reader.next();
			    if (reader.isWhiteSpace()) {
			        continue;
			    } else if (reader.isStartElement()) {
			    	element = reader.getName().getLocalPart();
			    } else if (reader.isCharacters() && element.equals("id")) {
			    	currentSongId = reader.getText();
			    } else if (reader.isCharacters() && element.equals("location")) {
			    	location = reader.getText();
			    	// If the current location is does not correspond to one of the files to be deleted,
			    	// then the current id is assigned as the newLastIdAssigned.
			    	if (!songPathsToDelete.contains(location)) {
			    		xmlNewLastIdAssigned = currentSongId;
			    	}
			    } else if (reader.isEndElement() && reader.getName().getLocalPart().equals("songs")) {
			    	break;
			    }
			}
			// Closes xml reader.
			reader.close();
			
			// Converts the file number to an int and returns the value. 
			return Integer.parseInt(xmlNewLastIdAssigned);
		} catch (Exception e) {
			e.printStackTrace();
			return 0;
		}
    }
	
	public static void deleteSongFromPlaylist(int selectedPlayListId, int selectedSongId) {
        try {
			DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
			Document doc = docBuilder.parse(Resources.JAR + "library.xml");
			
            XPathFactory xPathfactory = XPathFactory.newInstance();
            XPath xpath = xPathfactory.newXPath();
            
            // Finds the node with the song id for the selected song in the selected play list for removal.
            String query = "/library/playlists/playlist[@id='" + selectedPlayListId + "']/songId[text() = '" + selectedSongId + "']";
            XPathExpression expr = xpath.compile(query);
            Node deleteSongNode = (Node) expr.evaluate(doc, XPathConstants.NODE);
            
            // Removes the node corresponding to the selected song.
            deleteSongNode.getParentNode().removeChild(deleteSongNode);
                    
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            DOMSource source = new DOMSource(doc);
            File xmlFile = new File(Resources.JAR + "library.xml");
            StreamResult result = new StreamResult(xmlFile);
            transformer.transform(source, result);
            
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
	
	public static void deletePlaylistFromXML(int selectedPlayListId) {		
        try {
			DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
			Document doc = docBuilder.parse(Resources.JAR + "library.xml");
			
            XPathFactory xPathfactory = XPathFactory.newInstance();
            XPath xpath = xPathfactory.newXPath();
            
            // Finds the node with the play list id for removal.
            String query = "/library/playlists/playlist[@id='" + selectedPlayListId + "']";
            XPathExpression expr = xpath.compile(query);
            Node deleteplaylistNode = (Node) expr.evaluate(doc, XPathConstants.NODE);
            
            // Removes the node corresponding to the selected song.
            deleteplaylistNode.getParentNode().removeChild(deleteplaylistNode);
                    
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            DOMSource source = new DOMSource(doc);
            File xmlFile = new File(Resources.JAR + "library.xml");
            StreamResult result = new StreamResult(xmlFile);
            transformer.transform(source, result);
            
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}
}