package honk.tools.anidb;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.io.IOUtils;

public class DBConnection implements AniDBConnection {
    private static final long TIMEOUT = 15 * 1000;


    private static final int FILE_FCODE = 0 //
            + bit(1) //aid
            + bit(16) // dub Language
            + bit(17) // sub Language
            + bit(18) // quality
            + bit(19) // source
            + bit(25) // file type
            + bit(26) // length
            + bit(27); // description

    private static final int FILE_ACODE = bit(8)/*epNo*/+ bit(9)/*epName*/;

    private static final int ANIME_ACODE = 0 //
            + bit(1) // episodes
            + bit(2) // normal eps
            + bit(3) // special eps
            + bit(4) // rating
            + bit(5) // vote count
            + bit(6) // temp rating
            + bit(7) // temp vote count
            + bit(12) // anime-planet id
            + bit(13) // ann-id
            + bit(17) // pic name
            + bit(18) // year
            + bit(19) // type
            + bit(20) // romanji name
            + bit(21) // kanji name
            + bit(22) // english name
            + bit(24) // short name list
            + bit(26) // category list
            + bit(27); // related aid list


    private DatagramSocket socket;
    private DatagramPacket packet;
    private InetAddress address;

    private long identifier;
    private Map<String, String> answerMap = new HashMap<String, String>();
    private Map<String, String> requestMap = new HashMap<String, String>();
    private long lastSent = 0;

    private Timer timer = new Timer();

    private String user = Config.getString("anidb.username"); //$NON-NLS-1$
    private String pass = Config.getString("anidb.password"); //$NON-NLS-1$
    private final static String CLIENT = "webaom";
    private final static int CLIENT_VERSION = 119;
    //private final static String CLIENT = "honkstestclient";
    //private final static int CLIENT_VERSION = 1;

    private String sessionID;


    /**
     * send auth before sending any data
     */
    private boolean authed;

    private static int bit(int i) {
        return 1 << i;
    }

    private static void debug(String text) {
        if (true) {
            System.err.println(text);
        }
    }

    private void auth() {
        send("AUTH user=" + user + "&pass=" + pass + "&protover=3&client=" + CLIENT + "&clientver=" + CLIENT_VERSION
                + "&nat=1&comp=1&enc=utf8");
    }

    public DBConnection() throws IOException {
        socket = new DatagramSocket(48273);
        // send request
        address = InetAddress.getByName("api.anidb.info");


        Thread receiver = new Thread() {

            @Override
            public void run() {
                DatagramPacket packet = new DatagramPacket(new byte[1400], 1400);

                while (true) {
                    try {
                        socket.receive(packet);
                        byte[] buffer = packet.getData();
                        int length = packet.getLength();

                        if (buffer.length > 1 && buffer[0] == 0 && buffer[1] == 0) {
                            try {
                                Inflater dec = new Inflater();
                                dec.setInput(buffer, 2, length - 2);
                                byte[] result = new byte[10240];
                                length = dec.inflate(result);
                                dec.end();
                                buffer = result;
                            } catch (DataFormatException e) {
                                e.printStackTrace();
                            }
                        }


                        byte[] bufferCut = new byte[length];
                        System.arraycopy(buffer, 0, bufferCut, 0, length);


                        String data = new String(bufferCut);
                        final String[] message = data.split(" ", 2);
                        debug("< " + data);

                        if (message.length == 2) {
                            boolean handled = handle(message[0], message[1]);
                            if (!handled) {
                                answerMap.put(message[0], message[1]);
                            }
                        }

                        //remove data after 2 minutes
                        timer.schedule(new TimerTask() {
                            @Override
                            public void run() {
                                answerMap.remove(message[0]);
                            }
                        }, 2 * 60 * 1000);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }

            /**
             * Returns true if the message was consumed, false if it should be propagated.
             * @param id the messages id
             * @param string the message
             * @return true if the message was consumed, false if it should be propagated.
             */
            private boolean handle(final String id, String string) {
                String[] data = string.split(" ", 2);
                if (data.length != 2) {
                    return false;
                }


                String code = data[0];
                if (code.equals("200") || code.equals("201")) {
                    sessionID = data[1].split(" ")[0];
                    debug("  found new session id: " + sessionID);
                    return true;
                } else if (code.equals("501") || code.equals("506")) {
                    auth();
                    new Thread() {
                        public void run() {
                            sendRaw(requestMap.get(id));
                        }
                    }.start();
                    return true;
                } else if (code.startsWith("5") || code.startsWith("6")) {
                    System.err.println("AniDB error: " + string);
                    System.exit(4);
                }
                return false;
            }
        };

        receiver.setDaemon(true);
        receiver.start();
    }

    long last;
    public AnimeInfo getAnime(int aid) {
        debug("getting anime " + aid);
        
        //throttle a bit
        if (last-System.currentTimeMillis() < 2000) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        last = System.currentTimeMillis();
        

        String query = "ANIME aid=" + aid + "&s=$$SESSIONID$$&acode=" + ANIME_ACODE;
        long id = send(query);

        String data = waitForData(id);

        if (data != null) {
            if (!data.contains("\n")) {
                debug("Received invalid response: Did not get any data.");
                return null;
            }
            data = data.split("\n")[1];


            int count = 18;
            String[] fileData = data.split("\\|", -1);
            if (fileData.length != count) {
                debug("Received invalid response: Found " + fileData.length + " data fields, expected "+count+".");
                return null;
            }


            int i = 0;
            int episodes = Integer.parseInt(fileData[i++]);
            int normalEpisodes = Integer.parseInt(fileData[i++]);
            int specialEpisodes = Integer.parseInt(fileData[i++]);
            int rating = Integer.parseInt(fileData[i++]);
            int voteCount = Integer.parseInt(fileData[i++]);
            int tempRating = Integer.parseInt(fileData[i++]);
            int tempVoteCount = Integer.parseInt(fileData[i++]);
            i++;//String anpId = fileData[i++];
            String annId = fileData[i++];
            String picName = fileData[i++];
            String year = fileData[i++];
            String type = fileData[i++];
            String romajiName = fileData[i++];
            String kanjiName = fileData[i++];
            String englishName = fileData[i++];
            String[] shortNameList = AnimeInfo.stringToArray(fileData[i++], AnimeInfo.SEPERATOR_SHORTNAME);
            String[] categoryList = AnimeInfo.stringToArray(fileData[i++], AnimeInfo.SEPERATOR_CATEGORY);
            String relations = fileData[i++];

            String preferedName = null;

            String description = getAnnDescription(annId);
            

            System.out.println(relations);

            return new AnimeInfo(aid, episodes, normalEpisodes, specialEpisodes, rating, voteCount, tempRating,
                    tempVoteCount, picName, year, type, preferedName, romajiName, kanjiName, englishName,
                    shortNameList, categoryList, description, relations);
        }
        return null;
    }

    
    
    /*private String getAnpInfo(String anpId) {
        if ("0".equals(anpId)) {
            return null;
        }
        //String url = "http://www.anime-planet.com/animerec/helpfulInfo.php?k="+anpId;
        String url = "http://www.anime-planet.com/anirec/anirecinfo.php";
        debug("Accessing "+url);
        try {
            HttpClient client = new HttpClient();
            HttpMethod method = new GetMethod(url);
            method.addRequestHeader(new Header("id1", anpId));
            method.addRequestHeader(new Header("id2", anpId));
            method.addRequestHeader(new Header("info", "relations"));
            client.executeMethod(method);
            
            StringWriter sw = new StringWriter();
            InputStream pageStream = method.getResponseBodyAsStream();
            IOUtils.copy(pageStream, sw);
            String page = sw.toString();
            
            System.out.println("X"+page);
            
            
            Pattern pattern = Pattern.compile(".*(<TABLE.*?>.*?</TABLE>).*", Pattern.MULTILINE | Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
            Matcher matcher = pattern.matcher(page);
            matcher.matches();
            String table = matcher.group(1);
            
            return table;
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        return null;
    }*/

    public static void main(String[] args) {
        try {
            //DBConnection con = new DBConnection();
            //con.getAnnDescription("2938");
            //System.out.println(con.getAnnDescription("5957"));
            //System.out.println(con.getAnnDescription("168"));
            //System.out.println(con.getAnnDescription("1914"));
            //System.out.println(con.getAnpInfo("84"));
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.exit(0);
    }
    
    /**
     * Returns the description for the given id from ann
     * @param annId 
     * @return the description for the given id from ann
     */
    private String getAnnDescription(String annId) {
        try {
            String url = "http://www.animenewsnetwork.com/encyclopedia/anime.php?id=" + annId;
            debug("Accessing "+url);

            
            HttpClient client = new HttpClient();
            HttpMethod method = new GetMethod(url);
            client.executeMethod(method);
            
            StringWriter sw = new StringWriter();
            InputStream pageStream = method.getResponseBodyAsStream();
            IOUtils.copy(pageStream, sw);
            String page = sw.toString();
            
            page.replaceAll("<STRONG>", "");
            page.replaceAll("</STRONG>", "");
            
            Pattern pattern = Pattern.compile(".*<DIV.*?>(Plot Summary\\:.*?</DIV>.*)", Pattern.MULTILINE | Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
            Matcher matcher = pattern.matcher(page);
            if (matcher.matches()) {
                String match = matcher.group(1);
                Pattern innerPattern = Pattern.compile("(.*)<DIV.*?>(.*?)</DIV>(.*)", Pattern.MULTILINE | Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
                Matcher m = innerPattern.matcher(match);
                while (m.matches()) {
                    match = m.group(1)+m.group(2)+m.group(3); 
                    m = innerPattern.matcher(match);
                    
                    //we stripped all divs for the plot summary
                    if (match.indexOf("</DIV>") < match.indexOf("<DIV")) {
                        break;
                    }
                }
                

                match = match.replaceAll("(?m)(?i)(?s)</DIV>.*", "");
                
                match = match.replaceAll("(?m)(?i)(?s)<[^>]*?>", "");
                match = match.replaceFirst("Plot Summary:", "");
                match = match.trim();
                match = match.replaceAll("\\s\\s", " ");
                return match;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public FileInfo getFile(int size, String hash) {
        debug("getting file " + size + "|" + hash);
        String query = "FILE size=" + size + "&ed2k=" + hash + "&s=$$SESSIONID$$&fcode=" + FILE_FCODE + "&acode="
                + FILE_ACODE;
        long id = send(query);

        String data = waitForData(id);

        if (data != null) {
            if (data.contains("320 NO SUCH FILE")) {
                return null;
            }
            if (!data.contains("220 FILE")) {
                debug("Received invalid response.");
                return null;
            }

            data = data.split("\n")[1];


            String[] fileData = data.split("\\|");
            if (fileData.length != 11) {
                debug("Received invalid response.");
                return null;
            }


            int fileID = Integer.parseInt(fileData[0]);
            int animeID = Integer.parseInt(fileData[1]);
            String dubLang = fileData[2];
            String subLang = fileData[3];
            String quality = fileData[4];
            String source = fileData[5];
            String fileType = fileData[6];
            int length = Integer.parseInt(fileData[7]);
            String description = fileData[8];
            String episodeNumber = fileData[9];
            String episodeName = fileData[10];

            return new FileInfo(size, hash, animeID, fileID, episodeNumber, episodeName, description, quality, subLang,
                    dubLang, length, fileType, source);
        }
        return null;
    }

    private String waitForData(long id) {
        String idString = Long.toString(id);
        long start = System.currentTimeMillis();
        while (System.currentTimeMillis() - start < TIMEOUT) {
            String data = answerMap.get(idString);
            if (data != null) {
                return data;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return null;
    }


    private long send(String message) {
        final long id;
        synchronized (this) {
            id = ++identifier;
        }


        message += "&tag=" + id;

        requestMap.put(Long.toString(id), message);
        sendRaw(message);


        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                requestMap.remove(Long.toString(id));
            }
        }, 2 * 60 * 1000);

        return id;
    }


    private void sendRaw(String message) {
        if (!authed) {
            authed = true;
            auth();
        }


        //spam protection
        while (System.currentTimeMillis() - lastSent < 2200) {
            try {
                Thread.sleep(System.currentTimeMillis() - lastSent);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        lastSent = System.currentTimeMillis();


        message = message.replace("$$SESSIONID$$", String.valueOf(sessionID));
        debug("> " + message);


        byte[] buffer = message.getBytes();
        packet = new DatagramPacket(buffer, buffer.length, address, 9000);

        try {
            socket.send(packet);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /* (non-Javadoc)
     * @see honk.tools.anidb.AniDBConnection#getAnimes()
     */
    public List<Integer> getAnimes() {
        return Collections.emptyList();
    }

    /* (non-Javadoc)
     * @see honk.tools.anidb.AniDBConnection#changePreferedName(int, java.lang.String)
     */
    public void changePreferedName(int aid, String string) {
        // do nothing
    }


}
