
/* Author: Josh Holtrop
 * GVSU, CS654, Project 1
 * Date: 2008-03-11
 */

import java.io.*;
import java.net.*;
import java.util.*;

/* KaZaClient implements a file server client which can connect
 * to a "Mini Napster Hub" (implemented in KaZaServer) and search
 * for files to download. The files shared by the KaZaClient are
 * stored in a directory and described by ".kaza" files. The format
 * of a .kaza description file is a plain text file containing two
 * lines, the first of which is the name of the actual file to
 * share and the second of which is a brief description of the file.
 */
public class KaZaClient
{
    public static final int LISTEN_PORT = 3443;
    private static int m_transferIndex = 0;

    private String m_sharedFolder;
    private boolean m_connected = false;
    private Socket m_socket;
    private DataOutputStream m_os;
    private FileServer m_fileServer;
    private HashMap<Integer, ClientDownloader> m_clientDownloaders;

    /* make a KaZaClient object using the given user name,
     * connection speed (Kbps), path to the directory of shared files,
     * and hostname of the MNH group leader to connect to */
    public KaZaClient(String userName, int kbps,
                      String sharedFolder, String server)
    {
        m_sharedFolder = sharedFolder;
        if (userName == null || userName.trim().equals(""))
            userName = "Anonymous";

        try {
            m_socket = new Socket(server, KaZaServer.LISTEN_PORT);
            m_os = new DataOutputStream(m_socket.getOutputStream());
            m_os.writeBytes("HELO " + userName + "\n");
            m_os.writeBytes("SPED " + kbps + "\n");
            File sharedDir = new File(m_sharedFolder);
            if (sharedDir.isDirectory())
            {
                String[] files = sharedDir.list();
                for (String fName : files)
                {
                    if (fName.endsWith(".kaza"))
                    {
                        /* we found a description file, publish this file */
                        FileInputStream fis = new FileInputStream(m_sharedFolder +
                                                                  File.separator +
                                                                  fName);
                        BufferedReader br = new BufferedReader(
                                                new InputStreamReader(fis));
                        String sharedFileName = br.readLine();
                        String sharedFileDesc = br.readLine();
                        m_os.writeBytes("DESC " + sharedFileName + "\n" +
                                      sharedFileDesc + "\n");
                    }
                }
            }
        } catch (Exception e) {
            return;
        }

        m_fileServer = new FileServer(LISTEN_PORT, m_sharedFolder);
        Thread fsThread = new Thread(m_fileServer);
        fsThread.start();
        m_connected = true;
        m_clientDownloaders = new HashMap<Integer, ClientDownloader>();
    }

    /* check if a given file download is still active */
    public boolean isDownloadActive(int index)
    {
        synchronized (m_clientDownloaders)
        {
            return m_clientDownloaders.containsKey(index);
        }
    }

    /* check if the client is connected */
    public boolean connected() { return m_connected; }

    /* close the client, sends QUIT message to MNH server */
    public void close()
    {
        if (m_connected)
        {
            try {
                m_os.writeBytes("QUIT\n");
                m_os.close();
                m_socket.close();
            } catch (Exception e) { }
            m_connected = false;
        }
    }

    /* this method is called when the user wants the client
     * to send a search message to its MNH group leader */
    public Vector<SearchResult> performSearch(String query)
    {
        Vector<SearchResult> results = new Vector<SearchResult>();
        try
        {
            m_os.writeBytes("SRCH 2 " + query + "\n");
            BufferedReader br = new BufferedReader(
                                    new InputStreamReader(
                                        m_socket.getInputStream()));
            for (;;)
            {
                String resultStr = br.readLine();
                if (resultStr.equals("."))
                    break;
                StringTokenizer tokens = new StringTokenizer(resultStr, "|");
                SearchResult result = new SearchResult();
                for (int i = 0; i < 5; i++)
                {
                    if (tokens.hasMoreTokens())
                    {
                        String t = tokens.nextToken();
                        switch (i)
                        {
                            case 0:
                                result.peerAddress = t;
                                break;
                            case 1:
                                result.userName = t;
                                break;
                            case 2:
                                result.speed = Integer.parseInt(t);
                                break;
                            case 3:
                                result.fileName = t;
                                break;
                            case 4:
                                result.fileDescription = t;
                                break;
                        }
                    }
                }
                results.add(result);
            }
        }
        catch (Exception e) { }

        /* sort the results by the connection speed */
        Object[] arr = results.toArray();
        java.util.Arrays.sort(arr, new Comparator() {
            public int compare(Object o1, Object o2) {
                return ((SearchResult)o2).speed - ((SearchResult)o1).speed;
            }
        });

        results = new Vector<SearchResult>();
        for (Object o : arr)
        {
            results.add((SearchResult) o);
        }

        return results;
    }

    /* SearchResult stores data fields describing a "hit" that is returned
     * by the MNH group leader */
    public class SearchResult
    {
        String peerAddress = "";
        String userName = "";
        int speed = 0;
        String fileName = "";
        String fileDescription = "";

        public String toString()
        {
            return "User: \"" + userName + "\" [" +
                   peerAddress + ", " + getSpeedString(speed) +
                   "], File: \"" +
                   fileName + "\" (" + fileDescription + ")";
        }

        /* val is measured in Kbps */
        private String getSpeedString(int val)
        {
            if (val > 999999)
                return (val/1000000) + " Gbps";
            if (val > 999)
                return (val / 1000) + " Mbps";
            return val + " Kbps";
        }
    }

    /* called by the user instructing the KaZaClient to begin
     * downloading a file from a peer user */
    public int downloadFile(String host, String fileName)
    {
        ClientDownloader cd = new ClientDownloader(host, fileName);
        synchronized (m_clientDownloaders)
        {
            m_clientDownloaders.put(cd.getIndex(), cd);
        }
        Thread t = new Thread(cd);
        t.start();
        return cd.getIndex();
    }

    /* this method returns a unique index for a given file download */
    public synchronized int getTransferIndex()
    {
        return m_transferIndex++;
    }

    /* this class implements a threaded file downloader so that a
     * file download can occur at the same time that the user is
     * searching for other files or downloading other files */
    public class ClientDownloader implements Runnable
    {
        private String m_host;
        private String m_fileName;
        private int m_index;

        public ClientDownloader(String host, String fileName)
        {
            m_host = host;
            m_fileName = fileName;
            m_index = getTransferIndex();
        }

        public int getIndex() { return m_index; }

        public void run()
        {
            FileOutputStream fos;
            try {
                fos = new FileOutputStream(m_fileName);
            } catch (Exception e) {
                return;
            }

            try
            {
                Socket socket = new Socket(m_host, LISTEN_PORT);
                DataOutputStream os = new DataOutputStream(socket.getOutputStream());
                DataInputStream is = new DataInputStream(socket.getInputStream());

                os.writeBytes(m_fileName + "\n");

                byte[] buff = new byte[1500];

                for (;;)
                {
                    int bytesRead = is.read(buff, 0, buff.length);
                    if (bytesRead < 0)
                        break;
                    fos.write(buff, 0, bytesRead);
                }
            }
            catch (Exception e) { }

            try {
                fos.close();
            } catch (Exception e) {
            }

            synchronized (m_clientDownloaders)
            {
                m_clientDownloaders.remove(getIndex());
            }
        }
    }
}
