/***************************************************************************
    Copyright          : (C) 2002 by Neoworks Limited. All rights reserved
    URL                : http://www.neoworks.com
 ***************************************************************************/
/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

import helliker.id3.MP3File;
import helliker.id3.ID3v2FormatException;
import java.util.*;
import java.io.*;
import com.neoworks.jukex.*;
import java.sql.*;
import java.net.URL;
import java.net.URLEncoder;
import java.net.MalformedURLException;

import com.neoworks.connectionpool.PoolManager;
import com.neoworks.mpeg.MP3FileFilter;
import com.neoworks.util.Getopts;
import com.neoworks.util.DirectoryFileFilter;

import java.util.regex.*;

import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.Category;

/**
 * Import MP3 metadata into the database
 *
 * @author Nigel Atkinson (<a href="mailto:nigel@neoworks.com">nigel@neoworks.com</a>)
 * @author Nick Vincent (<a href="mailto:nick@neoworks.com">nick@neoworks.com</a>)
 */
public class MP3Importer
{
	private static Category log = null;
	private static boolean logDebugEnabled = false;
	private static boolean logInfoEnabled = false;

	private static Properties attributeSubstitutions = null;
	private static long tracksImported = 0;
	private static long tracksSkipped = 0;

	private static Attribute attrArtist = null;
	private static Attribute attrTitle = null;
	private static Attribute attrAlbum = null;
	private static Attribute attrTrackNumber = null;
	private static Attribute attrBitrate = null;
	private static Attribute attrVBR = null;
	private static Attribute attrGenre = null;
	private static Attribute attrLength = null;
	private static Attribute attrSampleRate = null;
	private static Attribute attrBorken = null;
	private static Attribute attrPlayed = null;

	private static TrackStore ts = null;

	private static boolean isUsingRegex = false;
	private static Pattern regexpPattern = null;
	private static String replaceExpression = null;
	private static boolean recurseDirectories = false;
	private static boolean forceUpdate = false;

	/**
	 * Public constructor
	 */
	public MP3Importer()
	{
	}
	
	/**
	 * Entry point
	 *
	 * @param args the command line arguments
	 */
	public static void main(String[] args)
	{
		// initialise logging
		String propertiesFilePath = "logging.properties";
		
		//System.out.println( "Looking for properties file: " + propertiesFilePath );
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		//System.out.println( "Got classloader: " + loader.getClass().getName() );
		URL loggingPropertiesURL = loader.getResource( propertiesFilePath );
		if (loggingPropertiesURL == null)
		{
                        Properties loggingProps = new Properties();
                        loggingProps.put("log4j.rootCategory","WARN, A1");
                        loggingProps.put("log4j.appender.A1","org.apache.log4j.FileAppender");
                        loggingProps.put("log4j.appender.A1.File","System.err");
                        loggingProps.put("log4j.appender.A1.layout","org.apache.log4j.PatternLayout");

                        // Print the date in ISO 8601 format
                        loggingProps.put("log4j.appender.A1.layout.ConversionPattern","%d [%t] %-5p %c - %m%n");
			//System.out.println("Configuring logging without properties file");
                        PropertyConfigurator.configure(loggingProps);
		} else {
			//System.out.println( "Got URL: " + loggingPropertiesURL );
			PropertyConfigurator.configure( loggingPropertiesURL );
		}
		if ( args.length < 1 )
		{
			System.out.println("\nMP3Importer [-m matchexpression -r replaceexpression] [-f] -d directory");
			System.out.println("  -n Do not actually add or update any tracks");
			System.out.println("  -R Recurse directories");
			System.out.println("  -m Regular expression to match");
			System.out.println("  -r Regular expression to replace match with");
			System.out.println("  -s <attribute substitution propertiesfile>");
			System.out.println("  -f Force update");
			System.out.println("  -d Directory to import");
			System.exit( 0 );
		}
		// initialise logging category for this class		
		log = Category.getInstance(MP3Importer.class.getName());
		logDebugEnabled = log.isDebugEnabled();
		logInfoEnabled = log.isInfoEnabled();

		Getopts getopts = new Getopts( "m:r:d:fRns:" , args );
		
		// Compile the regular expressions
		if ( getopts.hasOption( 'r' ) || getopts.hasOption( 'm' ) )
		{
			if ( getopts.hasOption( 'r' ) && getopts.hasOption( 'm' ) )
			{
				System.out.println( "Match in URL: "+getopts.option('m') );
				System.out.println( "Replace with: "+getopts.option('r') );
				try
				{
					regexpPattern = Pattern.compile( getopts.option('m') );
					Matcher m = regexpPattern.matcher( "abcdefghijklmnopqrstuvwxyz" );
					replaceExpression = getopts.option('r');
					m.replaceAll( replaceExpression );	    // Test to make sure the replacement supplied doesn't throw an exception too.
					isUsingRegex = true;
				}
				catch ( PatternSyntaxException pse )
				{
					log.warn( "Could not parse the supplied regular expression", pse );
				}
			}
			else
			{
				System.out.println( "Both -r and -m options must be specified in order to enable replacement" );
				System.exit( 0 );
			}
		}

		if ( getopts.hasOption ('s'))
		{
			attributeSubstitutions = new Properties();
			try {
				File attributeSubstitutionsFile = new File(getopts.option('s'));
				attributeSubstitutions.load(new FileInputStream(attributeSubstitutionsFile));
			} catch (FileNotFoundException f) {
				log.warn("Could not find file " + getopts.option('s'), f);
			} catch (IOException e) {
				log.warn("IOException while loading propertiesfile", e);
			}
		}

		boolean dummy = true;
		if (!getopts.hasOption('n'))
		{
			dummy = false;
		}

		if (getopts.hasOption('R'))
		{
			recurseDirectories = true;
		}
		
		if (getopts.hasOption('f'))
		{
			forceUpdate = true;
		}

		java.util.Date startTime = new java.util.Date( System.currentTimeMillis() );
		//System.out.println("Getting the trackstore");
		ts = TrackStoreFactory.getTrackStore();
		//System.out.println("Done getting the trackstore");

		// Try and create the essential information
		try
		{
			attrArtist = ts.createAttribute( "Artist" , Attribute.TYPE_STRING );
			attrTitle = ts.createAttribute( "Title" , Attribute.TYPE_STRING );
			attrAlbum = ts.createAttribute( "Album" , Attribute.TYPE_STRING );
			attrTrackNumber = ts.createAttribute( "TrackNumber" , Attribute.TYPE_INT );
			attrBitrate = ts.createAttribute( "Bitrate" , Attribute.TYPE_INT );
			attrVBR = ts.createAttribute( "VBR" , Attribute.TYPE_INT );
			attrGenre = ts.createAttribute( "Genre" , Attribute.TYPE_STRING );
			attrLength = ts.createAttribute( "Length" , Attribute.TYPE_INT );
			attrSampleRate = ts.createAttribute( "SampleRate" , Attribute.TYPE_INT );
			attrPlayed = ts.createAttribute( "Played" , Attribute.TYPE_INT );
			attrBorken = ts.createAttribute( "borken" , Attribute.TYPE_INT );
		}
		catch ( Exception e )
		{
			log.warn( "Could not create required attributes because of an exception", e );
		}
		
		
		Iterator attributes = ts.getAttributes().iterator();
		
		System.out.println("\nAttributes Supported:");
		while ( attributes.hasNext() )
		{
			Attribute ca = (Attribute) attributes.next();
			System.out.println( "\t" + ca.getName() );
		}
		
		if (recurseDirectories)
		{
			System.out.println("\nRecursing sub-directories");
		}
		File mp3Dir = new File( getopts.option('d') );

		updateTracks(mp3Dir, dummy);
		
		java.util.Date endTime = new java.util.Date( System.currentTimeMillis() );
		double secondsTaken = ( endTime.getTime() - startTime.getTime() ) / 1000.0 ;
		
		double totalTracks = tracksImported + tracksSkipped;
		double timePerTrack = ( secondsTaken / totalTracks );
		double tracksPerSecond = ( totalTracks / secondsTaken );
				
		System.out.println( "Import started at "+startTime+" and finished at "+endTime );
		System.out.println( "Time taken: " + secondsTaken );
		System.out.println( "Tracks Imported: "+tracksImported+"    Tracks Skipped: "+tracksSkipped );
		System.out.println( "Efficiency: "+timePerTrack+" s/track    "+tracksPerSecond+" s/track " );
	}

	private static void updateTracks(File mp3Dir, boolean dummy)
	{
		if ( mp3Dir.isDirectory() )
		{
			System.out.println("\n'" +mp3Dir.getAbsolutePath() + "' is a directory");
			List subDirs = null;
			if (recurseDirectories)
			{
				subDirs = java.util.Arrays.asList( mp3Dir.listFiles( new DirectoryFileFilter() ) );
				if (logInfoEnabled) log.info("'" + mp3Dir.getAbsolutePath() + "' contains "+subDirs.size() + " sub directories");
				Iterator i = subDirs.iterator();
				while (i.hasNext())
				{
					updateTracks((File)i.next(), dummy);
				}
			}
			List mp3files = java.util.Arrays.asList( mp3Dir.listFiles( new MP3FileFilter() ) );
			if (logInfoEnabled) log.info( "'" + mp3Dir.getAbsolutePath() + "' contains "+mp3files.size()+" MP3 files" );

			Iterator iter = mp3files.iterator();
			while (iter.hasNext())
			{
				File currFile = (File) iter.next();
				updateTrack(currFile, dummy);
			}
		}
		else
		{
			log.warn( "'" + mp3Dir.getAbsolutePath()+ "' is not a directory" );
		}
	}

	private static void updateTrack(File currFile, boolean dummy)
	{
		URL url = null; 
		Map trackAttributeValues = new HashMap();

		try
		{
			MP3File currMP3File = new MP3File(currFile.getPath(),MP3File.ID3V1_ONLY);
			fillAttributeValueMap(currMP3File, trackAttributeValues, false);
			currMP3File.setTaggingType(MP3File.ID3V2_ONLY);
			fillAttributeValueMap(currMP3File, trackAttributeValues, true);
			url = currFile.toURL();
			
			// try and convert 
			if ( isUsingRegex )
			{
				String urlString = url.toString();
				Matcher matcher = regexpPattern.matcher( urlString );
				String rewrittenUrlString = matcher.replaceAll( replaceExpression );
				try 
				{ 
					url = new URL(rewrittenUrlString); 
				}
				catch ( Exception e ) 
				{ 
					log.warn( "Could not parse rewritten string \""+rewrittenUrlString+"\" into a valid URL because the URL library says ", e );
				}
			}
			String urlFilename = url.getFile().substring(1);
			System.out.println(urlFilename);
			String fixedUrlFilename = fixEncoding(URLEncoder.encode(urlFilename,"UTF-8"));
			try {
				url = new URL(url.getProtocol(),url.getHost(),"/" + fixedUrlFilename);
			} catch (Exception e) {
				log.warn( "Could not parse rewritten string \""+fixedUrlFilename+"\" into a valid URL because the URL library says ", e );
			}
			
			long currFileDate = currFile.lastModified();
			Track track = ts.getTrack( url );
			if ( track == null )
			{
				System.out.println("[+] "+url);
				if (!dummy)
				{
					Track newTrack = ts.storeTrack( url , new java.util.Date( currFileDate ) );
			
					newTrack.addAttributeValue( attrArtist , attrArtist.getAttributeValue( (String)trackAttributeValues.get("Artist") ) );
					newTrack.addAttributeValue( attrTitle , attrTitle.getAttributeValue( (String)trackAttributeValues.get("Title") ) );
					newTrack.addAttributeValue( attrAlbum , attrAlbum.getAttributeValue( (String)trackAttributeValues.get("Album") ) );
					newTrack.addAttributeValue( attrBitrate , attrBitrate.getAttributeValue( ((Integer)trackAttributeValues.get("Bitrate")).intValue() ) );
				
					newTrack.addAttributeValue( attrVBR , attrVBR.getAttributeValue(((Integer)trackAttributeValues.get("VBR")).intValue()));
					newTrack.addAttributeValue( attrGenre , attrGenre.getAttributeValue((String)trackAttributeValues.get("Genre")));
					newTrack.addAttributeValue( attrLength , attrLength.getAttributeValue(((Integer)trackAttributeValues.get("Length")).intValue()));
					newTrack.addAttributeValue( attrSampleRate , attrSampleRate.getAttributeValue(((Integer)trackAttributeValues.get("SampleRate")).intValue()));
					newTrack.addAttributeValue( attrBorken , attrBorken.getAttributeValue(0));
					newTrack.addAttributeValue( attrPlayed , attrPlayed.getAttributeValue(0));
				}
				tracksImported++;
			}
			else
			{
				long trackStoreDate = track.getUpdatedDate().getTime();
				
				if ( ( trackStoreDate < currFileDate ) || forceUpdate )
				{
					// Update the information as the file is newer than what we have
					
					System.out.println("[U] "+url);
					if (!dummy)
					{
						track.replaceAttributeValues( attrArtist , attrArtist.getAttributeValue( (String)trackAttributeValues.get("Artist") ) );
						track.replaceAttributeValues( attrTitle , attrTitle.getAttributeValue( (String)trackAttributeValues.get("Title") ) );
						track.replaceAttributeValues( attrAlbum , attrAlbum.getAttributeValue( (String)trackAttributeValues.get("Album") ) );
						track.replaceAttributeValues( attrBitrate , attrBitrate.getAttributeValue( ((Integer)trackAttributeValues.get("Bitrate")).intValue() ) );

						track.replaceAttributeValues( attrVBR , attrVBR.getAttributeValue(((Integer)trackAttributeValues.get("VBR")).intValue()));
						track.replaceAttributeValues( attrGenre , attrGenre.getAttributeValue((String)trackAttributeValues.get("Genre")));
						track.replaceAttributeValues( attrLength , attrLength.getAttributeValue(((Integer)trackAttributeValues.get("Length")).intValue()));
						track.replaceAttributeValues( attrSampleRate , attrSampleRate.getAttributeValue(((Integer)trackAttributeValues.get("SampleRate")).intValue()));

						track.setUpdatedDate( new java.util.Date( currFileDate ) );
					}
					tracksImported++;
				}
				else
				{
					// Skip the file.  We have alread seen this one.
					
					System.out.println("[ ] "+url);
					
					tracksSkipped++;
				}
			}
			
			//System.out.println("Created this track:\n"+newTrack);
		}
		catch ( MalformedURLException mue )
		{
			log.warn( "Could not convert path ["+currFile.getPath()+"] into a valid URL", mue );
		}
		catch ( IOException ioe )
		{
			log.warn( "Couldn't open file with path ["+currFile.getPath()+"]", ioe );
		}
		catch ( ID3v2FormatException e )
		{
			log.warn( "File at ["+currFile.getPath()+"] has a corrupt ID3 tag", e );
		}
		catch ( Exception e )
		{
			log.warn( "Exception encountered", e );
		}
	}

	/**
	 * Convert application/x-www-form-urlencoded String to a URL safe String
	 *
	 * @param encStr The application/x-www-form-urlencoded String
	 * @return The URL safe String
	 */
	private static String fixEncoding(String encStr)
	{
		Pattern p = Pattern.compile("\\+");
		Matcher m = p.matcher(encStr);
		return m.replaceAll("%20");
	}

	private static void fillAttributeValueMap(MP3File mp3, Map map, boolean useRewrites) throws ID3v2FormatException
	{
		Object o = null;
		o = map.get("Artist");
		if (o != null && !((String)o).equals(""))
		{ } else {
			map.put("Artist",mp3.getArtist());
		}
		o = map.get("Title");
		if (o != null && !((String)o).equals(""))
		{ } else {
			map.put("Title",mp3.getTitle());
		}
		o = map.get("Album");
		if (o != null && !((String)o).equals(""))
		{ } else {
			map.put("Album",mp3.getAlbum());
		}
		o = map.get("Genre");
		if (o != null && !((String)o).equals(""))
		{ } else {
			map.put("Genre",mp3.getGenre());
		}
		o = map.get("Track");
		if (o != null && !((String)o).equals(""))
		{ } else {
			map.put("Track",mp3.getTrackString());
		}
		o = map.get("Bitrate");
		if (o != null && !((Integer)o).equals(new Integer(-1)))
		{ } else {
			map.put("Bitrate",new Integer(mp3.getBitRate()));
		}
		o = map.get("VBR");
		if (o != null)
		{ } else {
			map.put("VBR",new Integer(mp3.isVBR()?1:0));
		}
		o = map.get("Length");
		if (o != null && !((Integer)o).equals(new Integer(-1)))
		{ } else {
			map.put("Length",new Integer((int)mp3.getPlayingTime()));
		}
		o = map.get("SampleRate");
		if (o != null && !((Integer)o).equals(new Integer(-1)))
		{ } else {
			map.put("SampleRate",new Integer(mp3.getSampleRate()));
		}
		o = map.get("Copyrighted");
		if (o != null)
		{ } else {
			map.put("Copyrighted",new Integer(mp3.isMPEGCopyrighted()?1:0));
		}

		// make substitutions as required
		if (attributeSubstitutions != null && useRewrites)
		{
			Enumeration names = attributeSubstitutions.propertyNames();
			String name = null;
			String expr = null;
			String [] tokens = null;
			char delimiter;
			while (names.hasMoreElements())
			{
				name = (String)names.nextElement();
				expr = attributeSubstitutions.getProperty(name);
				delimiter = expr.charAt(0);
				tokens = expr.split(String.valueOf(delimiter));

				// The tokens array is going to contain either 3 or 4 elements:
				// An empty string, the regex to match, the replacement string and possibly a "g"
				
				o = map.get(name);
				if (o != null && o instanceof String)
				{
					if (tokens.length == 4 && tokens[2].equals("g"))
					{
						if (logInfoEnabled) log.info("Rewriting attribute " + name + ": /" + tokens[1] + "/" + tokens[2] + "/g" + " [" + ((String)o)+ " -> "+((String)o).replaceAll(tokens[1],tokens[2])+ "]");
						map.put(name,((String)o).replaceAll(tokens[1],tokens[2]));
					} else if (tokens.length == 3) {
						if (logInfoEnabled) log.info("Rewriting attribute " + name + ": /" + tokens[1] + "/" + tokens[2] + "/" + " [" + ((String)o)+ " -> "+((String)o).replaceAll(tokens[1],tokens[2])+ "]");
						map.put(name,((String)o).replaceFirst(tokens[1],tokens[2]));
					} else {
						log.warn("Bad regular expression: " + tokens.length + " tokens:");
						for (int i = 0; i < tokens.length; i++)
						{
							log.warn("\t" + tokens[i]);
						}
					}
				}
			}
		}
	}
}
