import java.io.*;
import java.util.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;


// Notes:
//
// The parsing generates a structure like you would expect and assigns it
// to the vector finalElements. Each structure has a Map for every <dict>
// element, an ArrayList for every <array> element, and a string for pretty
// much everything else. Key value pairs in dicts are inserted correctly.
// printTrack makes things a little more clear by showing how to print just
// the tracks, printThingy is a bit of a recursive printing hack to print
// the whole track.

// Possible Bugs:
//
// 1. Specifics of various formats are not currently dealt with at all.
// For example, <data> elements are supposed to be base64 encoded, but
// we just ignore that for the moment (who knows what data it represents
// anyway).
// 2. It may be possible for a <plist> to have more than one <dict> or
// <array> element. Presently I assume there is only one.
// 3. The whole string comparison thing for class names is a bit of a hack
// but seems fairly workable for the moment.

public class ITunesPlaylistReader
{
    private ITunesXMLHandler myHandler;


    public ITunesPlaylistReader ()
    {
        try
        {
            myHandler = new ITunesXMLHandler();
        }
        catch (Exception e)
        {
            System.out.println(e);
            System.exit(1);
        }
    }


    public void read (String filename)
    {
        try
        {
            myHandler.read(filename);
        }
        catch (Exception e)
        {
            System.out.println(e);
            System.exit(1);
        }
    }

    public void write (String filename)
    {
        try
        {
            myHandler.write(filename);
        }
        catch (Exception e)
        {
            System.out.println(e);
            System.exit(1);
        }
    }

    public Iterator getPlaylist (String name)
    {
        List results = new ArrayList();

        Map tracks = (Map)(myHandler.getFirstElement().get(name));
        Iterator iter = tracks.values().iterator();
        while (iter.hasNext())
        {
            Map trackInfo = (Map)iter.next();
            results.add(new Track(
                (String)trackInfo.get("Artist"),
                (String)trackInfo.get("Name"),
                (String)trackInfo.get("Album"),
                (String)trackInfo.get("Genre"),
                ((Integer)trackInfo.get("Size")).intValue(),
                ((Integer)trackInfo.get("Total Time")).intValue(),
                ((Integer)trackInfo.get("Bit Rate")).intValue(),
                ((Integer)trackInfo.get("Track ID")).intValue()));
        }

        return results.iterator();
    }

    public Iterator getTracks ()
    {
        return getPlaylist("Tracks");
    }


    private class ITunesXMLHandler extends DefaultHandler
    {
        private XMLReader xr;
        private String curtext;
        private Stack varstack;
        private Stack keynames;
        private List finalElements;

        
        public ITunesXMLHandler () throws Exception
        {
            xr = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
            xr.setContentHandler(this);
            xr.setErrorHandler(this);
        }


        public void read (String filename) throws Exception
        {
            xr.parse(new InputSource(new FileReader(filename)));
        }

        public void write (String filename) throws Exception
        {
            FileWriter out = new FileWriter(new File(filename));
            // write header
            out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
            out.write("<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n");
            out.write("<plist version=\"1.0\">\n");
            out.write("<dict>\n");
            // made up header data
            out.write("<key>Major Version</key><integer>1</integer>\n");
            out.write("<key>Minor Version</key><integer>1</integer>\n");
            out.write("<key>Application Version</key><string>4.5</string>\n");
            out.write("<key>Music Folder</key><string>file://localhost/Music/iTunes/iTunes%20Music/</string>\n");
            out.write("<key>Library Persistent ID</key><string>F3349961DC433C7B</string>\n");
            out.write("<key>Tracks</key>\n");
            out.write("<dict>\n");
            // write tracks
            Iterator iter = getTracks();
            while (iter.hasNext())
            {
                Track track = (Track)iter.next();
                out.write("<key>" + track.getID() + "</key>\n");
                out.write("<dict>\n");
                out.write("<key>Track ID</key><integer>" + track.getID() + "</integer>\n");
                out.write("<key>Name</key><string>" + track.getTitle() + "</string>\n");
                out.write("<key>Artist</key><string>" + track.getArtist() + "</string>\n");
                out.write("<key>Album</key><string>" + track.getAlbum() + "</string>\n");
                out.write("<key>Genre</key><string>" + track.getGenre() + "</string>\n");
                out.write("<key>Size</key><string>" + track.getSize() + "</string>\n");
                out.write("<key>Total Time</key><integer>" + track.getTime() + "</integer>\n");
                out.write("<key>Bit Rate</key><integer>" + track.getBitRate() + "</integer>\n");
                out.write("</dict>\n");
            }
            out.write("</dict>");
            out.write("</dict>");
            out.write("</plist>");

            out.close();
        }

        public void startDocument ()
        {
            varstack = new Stack();
            keynames = new Stack();
            finalElements = new ArrayList();
            curtext = "";
        }

        public void endDocument ()
        {}

        public void startElement (String uri, String name, String qName,
                                  Attributes atts)
        {
            if (name.equals("dict"))
            {
                varstack.push(new TreeMap());
            }
            else if (name.equals("array"))
            {
                varstack.push(new ArrayList());
            }

            // get rid of current text since all text in itunes format is between elements
            curtext = "";
        }

        public void endElement (String uri, String name, String qName)
        {
            if (name.equals("key"))
            {
                // add key to stack of key names, so we can use key names even when
                // key corresponds to a container element like <dict> or <array>.
                keynames.push(curtext);
            }
            else
            {
                Object o;
                if (name.equals("dict") || name.equals("array"))
                {
                    // just finished array or dict, then pop it and add it to parent
                    o = varstack.pop();
                    if (varstack.empty())
                    {
                        // if no parent, just add it to top level
                        finalElements.add(o);
                        return;
                    }
                }
                else if (name.equals("integer"))
                {
                    o = new Integer(Integer.parseInt(curtext));
                }
                else if (name.equals("real"))
                {
                    o = new Double(Double.parseDouble(curtext));
                }
                else if (name.equals("true") || name.equals("false"))
                {
                    o = Boolean.valueOf(name);
                }
                else // if (name.equals("date") || name.equals("string") || name.equals("data"))
                {
                    // add date, string, and data values as strings
                    // this is not always the right thing to do because 
                    // <data> elements really have base64 encoded data
                    // but since nothing is being done with it,
                    // a string representation is fine
                    o = curtext;
                }

                if (varstack.empty() && name.equals("plist"))
                {
                    // if stack is empty and <plist> is ending, do nothing more
                    return;
                }
                else if (varstack.empty())
                {
                    System.out.println("Unexpected " + name + " when varstack "
                                       + "was empty.");
                    System.exit(1);
                }

                if (varstack.peek() instanceof Map)
                {
                    ((TreeMap)varstack.peek()).put(keynames.pop(), o);
                }
                else if (varstack.peek() instanceof List)
                {
                    ((ArrayList)varstack.peek()).add(o);
                }
            }

            curtext = "";
        }

        public void characters (char[] allText, int start, int length)
        {
            // allText is all characters parsed in the segment,
            // the characters between start and start+length
            // are those that were not part of tag or attribute
            curtext += new String(allText, start, length);
        }

        public Map getFirstElement ()
        {
            return (Map)finalElements.get(0);
        }

        private String replace (String arg)
        {
            // convert UNICODE characters to plain text
            if (arg == null) return "null";
            else return arg.replaceAll("&", "&#38;");
        }
    }

    public static void main (String args[]) throws Exception
    {
        ITunesPlaylistReader player = new ITunesPlaylistReader();

        player.read("library.xml");
        player.write("output.xml");
    }
}
