package kr.midireader.krwriter;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

/**
 * Unpackages a kr file. TODO: maybe rewrite this, it's ugly.
 */
public class KRUnpackager {
	private static final String LTF_FILE =  "songs/songname/songname.ltf";
	private static final String WEIGHTS_FILE =  "songs/songname/songname_weights.bin";
	private static final String LIP_FILE =  "songs/songname/gen/songname_lip.rnd";

	public KRUnpackager()
	{
	}
	
	
	public static void main(String [] argv) throws Exception
	{
		if(argv.length == 0)
		{
			usageAndExit(null);
		}
		
		
		int argIndex = 0;
		
		//true if song should be overwritten
		boolean overwrite = false;
		
		Map<String, File> otherFilesToCopy = new HashMap<String, File>();
		
		File songsDta = null;
		File dbDta = null;

		while(true)
		{
			if(argv[argIndex].equals("-o"))
			{
				overwrite = true;
				argIndex ++;
			}
			else if(argv[argIndex].equals("-ltf"))
			{
				otherFilesToCopy.put(LTF_FILE, new File(argv[argIndex+1]));
				argIndex +=2;
			}
			else if(argv[argIndex].equals("-weights"))
			{
				otherFilesToCopy.put(WEIGHTS_FILE, new File(argv[argIndex+1]));
				argIndex +=2;
			}
			else if(argv[argIndex].equals("-lip"))
			{
				otherFilesToCopy.put(LIP_FILE, new File(argv[argIndex+1]));
				argIndex +=2;
			}
			else if(argv[argIndex].equals("-songs"))
			{
				 songsDta = new File(argv[argIndex+1]);
 				 argIndex +=2;
			}
			else if(argv[argIndex].equals("-db"))
			{
				dbDta = new File(argv[argIndex+1]);
				argIndex +=2;
			}
			else break;
		}
		
		if(songsDta == null)
			usageAndExit("-songs <songs.dta file> must be specified");
		if(dbDta == null)
			usageAndExit("-db <db.dta file> must be specified");
		
		String dir = ".";
		if(new File(argv[argIndex]).isDirectory())
			dir = argv[argIndex++];

		findOtherFiles(new File(dir), otherFilesToCopy);
		
		if(otherFilesToCopy.size() < 3)
			
			usageAndExit("Either -ltf, -weights, and -lip must be specified or the directory to unpack to "
					+"must contain the files <song_abbr>_weights.bin, <song_abbr>.ltf, and <song_abbr>_lip.rnd. " +
							"You can find these files under the songs directory in your Karaoke Revolution distribution"+
					"\nOnly found "+otherFilesToCopy.keySet());
			
		String [] songs = new String [argv.length - argIndex];
		
		for(int i = 0; i < songs.length; i++)
		{
			songs[i] = argv[i + argIndex];
		}

		String songsDtaText = readText(new FileInputStream(songsDta)); 
		String dbDtaText = readText(new FileInputStream(dbDta)); 
		
		for(String songFile : songs)
		{
			System.out.println("Writing songFile '"+songFile+"'");
			
			String lastSongsDtaText = songsDtaText;
			String lastDbDtaText = dbDtaText;
			
			//just grab 'abc' from 'foo/abc.zip'
			String songName = songFile.replaceFirst(".*?([^\\\\/]*)\\.zip$", "$1").toLowerCase();
			
			boolean error = false;
			
			if(songName.matches("[^a-zA-Z0-9]"))
			{
				System.err.println("Error, songFile '"+songFile+"' must only contain alphabetical or numeric characters");
				error = true;
			}
			if(songName.length() > 8)
			{
				System.err.println("Error, songFile '"+songFile+"' must be at most 8 characters");
				error = true;
			}
			
			ZipFile zf = new ZipFile(songFile);


			if(error)
			{
				System.err.println("Skipping '"+songFile+"' due to previous errors");
				continue;
			}
			
			
			//copy the "other" files
			for(String krFile : otherFilesToCopy.keySet())
			{
				String f = convertKRPath(dir+File.separator+krFile, songName);

				copyFile(f, new FileInputStream(otherFilesToCopy.get(krFile)));
			}

			for(Enumeration<? extends ZipEntry> zee = zf.entries(); zee.hasMoreElements();)
			{
				ZipEntry ze = zee.nextElement();
				
				if(ze.isDirectory())
					continue;
				else if(ze.getName().equals(KRPackager.SONGS_DTA_ENTRY))
				{
					String entry = readText(zf.getInputStream(ze));
					entry = updateEntry(entry, songName);
					songsDtaText = addEntryToSongsDta(songsDtaText, entry, songName, overwrite);
				}
				else if(ze.getName().equals(KRPackager.DB_DTA_ENTRY))
				{
					String entry = readText(zf.getInputStream(ze));
					
					entry = updateEntry(entry, songName);					
					dbDtaText = addEntryToDbDta(dbDtaText, entry, songName, overwrite);
				}
				else
				{
					File f = new File(convertKRPath(dir+File.separator+ze.getName(), songName));
					if(f.exists())
					{
						if(!overwrite)
						{
							System.err.println("Song name for song file '"+songFile+"' already exists, skipping( use -o to override, or change the name of the song file)");
							
							//revert the db.dta and songs.dta incase they were changed
							songsDtaText = lastSongsDtaText;
							dbDtaText = lastDbDtaText;
							
							break;
						}
					}
					
					//copy the file
					copyFile(f.toString(), zf.getInputStream(ze));
				}
			}
		}
		
		writeFile(songsDta, songsDtaText);
		writeFile(dbDta, dbDtaText);
	}
	
	private static void usageAndExit(String message) {
		if(message != null)
			System.err.println(message+"\n");
		System.err.println("Usage: KRUnpackager [-o] [-ltf *.ltf file] [-weights *_weights.bin file] [-lip *_rnd.lip file] <-songs songs.dta file> <-db db.dta file> [directory to unzip to] <krsong1.zip> [krsong2.zip] ...");
		System.err.println(" The songs.dta and db.dta files will be patched to include the additional songs");
		System.err.println(" [directory to unzip to] must be the root directory of where you want to install kr to, this defaults to the current directory");
		System.err.println(" Either -ltf, -weights, and -lip must be specified or the directory to unpack to "
					+"must contain the files <song_abbr>_weights.bin, <song_abbr>.ltf, and <song_abbr>_lip.rnd for at least one song");
		System.err.println(" -o If specified, and the name of the zip file is the same as an existing entry in the songs.dta file or db.dta file, the old " +
				"entry will be replaced. Otherwise, the new entry will not unpack.");
		System.err.println(" Note: To add a new song with the same name as an old song, rename the zip file to another name (names must be a maximum of 8 " +
				"characters excluding the '.zip' extension and must only contain alphabetic or numeric characters)");
		System.exit(1);
		
	}


	/**
	 * This searches for the other files that are needed to copy into the song
	 * @param otherFilesToCopy 
	 */
	private static void findOtherFiles(File startDir, Map<String, File> otherFilesToCopy) {
		ArrayList<File> dirQueue = new ArrayList<File>();
		
		dirQueue.add(startDir);
		while(dirQueue.size() > 0 && otherFilesToCopy.size() < 3)
		{
			File dir = dirQueue.remove(dirQueue.size() - 1);
			for( File f : dir.listFiles())
			{
				if(f.isDirectory())
					dirQueue.add(f);
				else if(f.getName().matches(".*\\.ltf$") && !otherFilesToCopy.containsKey(LTF_FILE))
				{
					otherFilesToCopy.put(LTF_FILE, f);
				}
				else if(f.getName().matches(".*_weights\\.bin$") && !otherFilesToCopy.containsKey(WEIGHTS_FILE))
				{
					otherFilesToCopy.put(WEIGHTS_FILE, f);
				}
				else if(f.getName().matches(".*_lip\\.rnd$") && !otherFilesToCopy.containsKey(LIP_FILE))
				{
					otherFilesToCopy.put(LIP_FILE, f);
				}
			}
		}
		
	}


	/**
	 * Updates the name of the entry to match the song file name
	 */
	public static String updateEntry(String entry, String newName) {
		return entry.replaceFirst("^\\(\\s*[a-zA-Z0-9]{1,8}", "("+newName);
	}


	private static void copyFile(String fileName, InputStream is) throws IOException {
		new File(fileName).getParentFile().mkdirs();
		FileOutputStream os = new FileOutputStream(fileName);
		
		byte [] buf = new byte[4096];
		
		int count;
		
		while((count = is.read(buf)) > 0)
		{
			os.write(buf,0,count);
		}
		
		os.close();
		is.close();
	}


	private static void writeFile(File songsDta, String songsDtaText) throws IOException {
		FileWriter fw = new FileWriter(songsDta);
		
		
		fw.write(songsDtaText);
		
		fw.close();
	}

	/**
	 * Adds the given entry to the db.dta file, replacing any previous one with the
	 * same song name
	 */
	private static String addEntryToDbDta(String dbDtaText, String entry, String songName, boolean overwrite) {
		
		dbDtaText =  addEntryToDtaFile("hi_scores", dbDtaText, entry, songName, overwrite);
		
		return updateKUnlockSong(dbDtaText, songName);
	}


	private static String updateKUnlockSong(String dbDtaText, String songName) 
	{
		//
		// Check if song already in unlocked song list
		//
		
		//separate the data into lines
		boolean kUnlockSongStarted = false;
		
		for(String fullLine : dbDtaText.split("\r\n"))
		{
			//eat any comments
			String line = fullLine.replaceFirst(";.*", "");
			
			if(!kUnlockSongStarted)
			{
				if(line.contains("(kUnlockSong"))
				{
					kUnlockSongStarted = true;
				}
			}
			else
			{
				if(line.contains(songName)) //if already present in unlocked song list
					return dbDtaText; //no change needed
				
				if(line.contains(")"))
					break;
			}
		}
		
		
		//it's not already there, so add it
		return dbDtaText.replaceFirst("\\(kUnlockSong *\r\n","(kUnlockSong\r\n      "+songName+"\r\n");
	}


	/**
	 * Adds the given entry to the db.dta file, replacing any previous one with the
	 * same song name
	 * @param overwrite 
	 */
	private static String addEntryToSongsDta(String songsDtaText, String entry, String songName, boolean overwrite) {
		
		return addEntryToDtaFile(null, songsDtaText, entry, songName, overwrite);
		
	}


	/**
	 * Adds the given entry to the db.dta file, replacing any previous one with the
	 * same song name
	 * @param overwrite 
	 */
	private static String addEntryToDtaFile(String startKeyword, String dtaText, String entry, String songName, boolean overwrite) {
		
		//separate the data into lines

		Pattern songStartPattern = Pattern.compile("\\( *([a-z0-9]+)");
		
		Pattern openClosePattern = Pattern.compile("[()]");
		
		entry = entry.replaceAll("([^\r])\n", "$1\r\n");
		
		int braceLevel = 0;
		boolean songStarted = false;
		boolean startKeywordFound = false;
		boolean currEntryIsTheSame = false; //the current entry is the same as the song name
		boolean entryAlreadyFound = false; //the song name has already been found in the file
		int lineNo = 0;
		
		int lastCommentStart = 0;
		
		if(startKeyword == null) 
		{
			startKeywordFound = true;
			braceLevel = 1; //set the brace level to 1 to match the keyword version
		}
		
		StringBuffer output = new StringBuffer();
		
		
		for(String fullLine : dtaText.split("\r\n"))
		{
			boolean suppressLine = false;
			
			lineNo ++;
			
			//eat any comments
			String line = fullLine.replaceFirst(";.*", "");
			
			//
			//find any braces on the line
			//
			Matcher openCloseMatcher = openClosePattern.matcher(line);
			
			while(openCloseMatcher.find())
			{
				if(openCloseMatcher.group().equals("("))
					braceLevel++;
				else braceLevel--;
			}
						
			// if we just started the special section
			if(!startKeywordFound && startKeyword != null && line.indexOf(startKeyword) != -1)
			{
				startKeywordFound = true;
				braceLevel = 1;
			}
			else if(startKeywordFound)
			{
				if(!songStarted)
				{
					if(braceLevel == 2)
					{
						Matcher songStartMatcher = songStartPattern.matcher(line);
						if(songStartMatcher.find())
						{
							songStarted = true;
							String currSongName = songStartMatcher.group(1);
							//System.out.println("curr song name is "+currSongName);
							if(currSongName.equals(songName))
							{
								entryAlreadyFound = currEntryIsTheSame = true;
								if(overwrite) //if the entry is already found, and were overwriting it,
									//	eat it and replace it with the new one
								{
									output.delete(lastCommentStart, output.length());
									suppressLine = true;
								}
								
							}
						}
						else
						{
							System.err.println("Confused at line "+lineNo+": "+line);
						}
					}
					else if(braceLevel == 0) //if we are leaving the section
						//started with the keyword (if there is no keyword, we write the entry
						//at the end)
					{
						if(startKeyword != null && (overwrite || !entryAlreadyFound))
							output.append("\r\n").append(entry);
						startKeywordFound = false; //done with the containing element
					}
				}
				else //song started
				{
					if(currEntryIsTheSame && overwrite) //if the entry is already found, and were overwriting it,
							//	eat it and replace it with the new one
						suppressLine = true;
					if(braceLevel == 1) //if song ended
					{
						songStarted = false;
						currEntryIsTheSame = false;
					}		
				}
			}
			
			if(!suppressLine)
			{
				lastCommentStart = output.length() + line.length();
				output.append(fullLine).append("\r\n");
			}
			
		} // for each line in the file
		
		//if we don't have a particular starting section, we write out the entry at the end
		if(startKeyword == null && (overwrite || !entryAlreadyFound))
			output.append("\r\n").append(entry);

		return output.toString();
		
	}
	
	private static String patchSongsDta(String songsDtaText, String entry) {
		return songsDtaText+"\r\n"+entry+"\r\n";
		
	}


	private static String readText(InputStream inputStream) throws IOException {
		StringBuffer text = new StringBuffer();
		
		Reader r = new InputStreamReader(inputStream);
		
		char [] buf = new char[4096];
		
		int count;
		
		while((count = r.read(buf)) > 0)
		{
			text.append(buf, 0, count);
		}
		
		r.close();
		
		return text.toString();
	}


	/**
	 * Converts the krpath to use the given songname 
	 */
	private static String convertKRPath(String zipPath, String songName) {
		char s = File.separatorChar;
		
		zipPath =  zipPath.replace('\\', s).replace('/', s);
		
		if(zipPath.contains("songs"))
		{
			//convert the songs directory to the songName(incase the song name was changed)
			// after the archive was created
			zipPath = zipPath.
				replaceFirst("songs([\\\\/])[^\\\\/]*", "songs$1"+songName);
		}
			
		//all kr files contains the song name as the first part, so we replace this with the
		//actual song name
		zipPath = zipPath.replaceFirst("[a-zA-Z0-9]{1,8}([^\\\\/]*)$", songName+"$1");
		
		return zipPath;
	}
}
