package kamiworldserver.control;

import kamiworldserver.KamiServer;
import kamiworldserver.elem.KamiMap;
import kamiworldserver.elem.KamiUser;
import kamiworldserver.elem.KamiLink;
import kamiworldserver.comm.KamiCommand;

import java.sql.Statement;
import java.sql.ResultSet;

import java.io.IOException;
import java.sql.SQLException;

import java.util.Iterator;
import java.util.ArrayList;
import java.util.HashMap;

import java.nio.ByteBuffer;

public class KamiProcessor extends Thread {

    public final static int nTimerTick = 50;
    public final static int nMoveSpeed = 5;
    public final static String sDefaultMap = "w/world/start.map";
    private KamiServer ks_Parent;
    private Statement sState;
    private ResultSet rsSet;
    private StringBuilder sQuery;
    private HashMap<String, KamiMap> hmLevels = null;
    private ArrayList<KamiUser> alPlayers;
    private int intP;
    private ArrayList<KamiCommand> alCommands;
    private Iterator<KamiCommand> alC;
    private KamiCommand kcCurrent;

    public KamiProcessor(KamiServer serv, HashMap hm) {
        ks_Parent = serv;
        hmLevels = hm;

        alPlayers = new ArrayList<KamiUser>();
        alCommands = new ArrayList<KamiCommand>();
    }

    @Override
    public void run() {

        while (true) {

            synchronized (this) {
                intP = 0;
                while (intP < alPlayers.size()) {
                    if (alPlayers.get(intP).process()) {
                        --intP;
                    }
                    intP++;
                }

            }

            //
            try {
                if(kcCurrent!=null) parse(kcCurrent);

                /*
                alC = alCommands.iterator();
                while (alC.hasNext()) {
                    parse(alC.next());
                    kcCurrent=null;
                }
                 */
                int i =0;
                    alCommands.trimToSize();
                    while(!alCommands.isEmpty() && i<alCommands.size() && alCommands.get(i)!=null) {
                        if( parse(alCommands.get(i)) ) {
                            alCommands.remove(i);
                            kcCurrent = null;
                            continue;
                        }
                        //else System.out.print(" Command not parsed: "+alCommands.get(i)+" size: "+alCommands.size());
                        kcCurrent=null;
                        ++i;
                    }
                //alCommands.clear();

                Thread.yield();
                Thread.sleep(10);

            } catch (IOException ex) {
                System.out.println("Failed to read or write to Stream. Message: " + ex);
                kcCurrent.getCaller().close();
                alCommands.remove(kcCurrent);

            } catch (java.sql.SQLException ex) {
                System.out.println("Failed to process requested command. Message: " + ex.toString());
                kamiworldserver.KamiServer.reconnect();

                //kcCurrent.getCaller().close();
                //alCommands.remove(kcCurrent);

            } catch (Exception ex) {
                System.out.println("Unknow error. Message: " + ex);
                kcCurrent.getCaller().close();
                alCommands.remove(kcCurrent);
            }

        }

    }
    // =========================================================================
    // Commands reading and writing
    //

    protected boolean parse(KamiCommand kc) throws IOException, SQLException {

        ByteBuffer bb;

        kcCurrent = kc; //information for current kami command are parsed below in getCommand()
        sQuery = new StringBuilder(255);
        KamiUser kuCaller = kc.getCaller();
        KamiMap kmUserMap;
        ArrayList<KamiUser> players;

        switch (kcCurrent.getID()) {


            // Logon
            //01: <- Entering the world with character: (ID:Int, CID:Byte, sUser:String, sPass:String) (to server)
            case 1:
                sQuery = sQuery.append("SELECT * FROM users WHERE id IN (SELECT id FROM users_chars WHERE id=").append(kcCurrent.get(0)).append(" AND cid=").append(kcCurrent.get(1)).append(") AND login=\"").append(kcCurrent.get(2)).append("\" AND password=\"").append(kcCurrent.get(3)).append("\"");
                rsSet = sState.executeQuery(sQuery.toString());
                if (!rsSet.next()) {
                    kuCaller.close();
                }
                System.out.println("Last query: " + sQuery.toString());

                sQuery.delete(0, 255);
                sQuery = sQuery.append("SELECT * FROM users_chars WHERE id=").append(kcCurrent.get(0)).append(" AND cid=").append(kcCurrent.get(1));
                rsSet = sState.executeQuery(sQuery.toString());
                rsSet.next();

                kuCaller.setUserName((String) kcCurrent.get(2));
                kuCaller.setID(rsSet.getInt("id"));
                kuCaller.setData(rsSet.getString("name"), rsSet.getString("sheet_file"), rsSet.getString("colors"));
                kuCaller.setLocation(rsSet.getShort("posx"), rsSet.getShort("posy"));
                moveUserTo(rsSet.getString("map"), kuCaller);

                // 02: -> User accepted, loading character: (name:String, map:String,
                //	posx:Int, posy:Int, sheet_file:String,
                //	colors:String, class:Byte, level:Byte, stats:Int, hp:Int, sp:Int, exp:Int,
                //	gold:Int) (to client)
                bb = ByteBuffer.allocate(5022);
                bb.put(getUTF(kuCaller.getName()));
                bb.put(getUTF(rsSet.getString("map")));
                bb.putShort(kuCaller.getX());
                bb.putShort(kuCaller.getY());
                bb.put(getUTF(kuCaller.getFile()));
                bb.put(getUTF(kuCaller.getColors()));
                bb.put(rsSet.getByte("class"));
                bb.put(rsSet.getByte("level"));
                bb.putInt(rsSet.getInt("stats"));
                bb.putInt(rsSet.getInt("hp"));
                bb.putInt(rsSet.getInt("sp"));
                bb.putInt(rsSet.getInt("exp"));
                bb.putInt(rsSet.getInt("gold"));

                kcCurrent.getCaller().sendCommand(2, bb.array(), bb.position());
                System.out.println("C001 send: Log-on accepted.");
                System.out.println("Last query: " + sQuery.toString());

                return true;
            // Following link
            //03: <- Change map (link_id:Number) (to server)
            case 3:

                KamiLink kl = kcCurrent.getCaller().getMap().getLink(kcCurrent.get(0).toString());

                if (kl.notInLink((Short) kcCurrent.get(1), (Short) kcCurrent.get(2))) {
                    return false;
                }

                synchronized (HashMap.class) {
                    if (kl == null || hmLevels.get(kl.map) == null) {
                        kuCaller.sendID(7);
                        return true;
                    }
                    if( !kl.validLink() ) {
                        hmLevels.remove(kl.map);
                        kuCaller.sendID(7);
                        return true;
                    }
                }

                kuCaller.setLocation(kl.getNX((Short) kcCurrent.get(1)), kl.getNY((Short) kcCurrent.get(2)));

                bb = ByteBuffer.allocate(2048);
                bb.put(getUTF(kl.map));
                bb.putShort(kuCaller.getX());
                bb.putShort(kuCaller.getY());
                kuCaller.sendCommand(4, bb.array(), bb.position());

                kcCurrent.setID(13);
                parse(kcCurrent);

                kuCaller.setTime(System.currentTimeMillis());
                //if(kuCaller.getDir()>8) kuCaller.setDir( (byte)(kuCaller.getDir()-8) );

                moveUserTo(kl.map, kuCaller);

                return true;

            //Request players
            //10: Empty
            case 10:
                kmUserMap = kcCurrent.getCaller().getMap();
                players = kmUserMap.getPlayers();
                synchronized (players) {

                    ByteBuffer tmpbb = ByteBuffer.allocate(2048);
                    tmpbb.putInt(kuCaller.getID());
                    tmpbb.putShort(kuCaller.getX());
                    tmpbb.putShort(kuCaller.getY());
                    tmpbb.put(getUTF(kuCaller.getName()));
                    tmpbb.put(getUTF(kuCaller.getFile()));
                    tmpbb.put(getUTF(kuCaller.getColors()));

                    Iterator<KamiUser> itrKU = players.iterator();
                    KamiUser kp;
                    while (itrKU.hasNext()) {
                        kp = itrKU.next();
                        if (kp == kuCaller) {
                            continue;
                        }
                        bb = ByteBuffer.allocate(2048);
                        bb.putInt(kp.getID());
                        bb.putShort(kp.getX());
                        bb.putShort(kp.getY());
                        bb.put(getUTF(kp.getName()));
                        bb.put(getUTF(kp.getFile()));
                        bb.put(getUTF(kp.getColors()));

                        kuCaller.sendCommand(11, bb.array(), bb.position());
                        kp.sendCommand(11, tmpbb.array(), tmpbb.position());
                    }
                }

                kuCaller.setTime(System.currentTimeMillis());
                System.out.println("C010 send: Players listing send.");
                return true;

            //Broadcast "remove me"
            //12 comes from the server, not a client and is ment to be "passed along"
            //12: -> Removes player (ID:Number) (to client)
            case 12:

                sQuery = sQuery.append("UPDATE users_chars SET posx=").append(kuCaller.getX()).append(", posy=").append(kuCaller.getY()).append(", map=\"").append(kuCaller.getMap().toString()).append("\" WHERE id=").append(kuCaller.getID());
                sState.execute(sQuery.toString());

                kmUserMap = kcCurrent.getCaller().getMap();
                players = kmUserMap.getPlayers();
                synchronized (players) {

                    ByteBuffer tmpbb = ByteBuffer.allocate(16);
                    tmpbb.putInt(kuCaller.getID());

                    Iterator<KamiUser> itrKU = players.iterator();
                    KamiUser kp;
                    while (itrKU.hasNext()) {
                        kp = itrKU.next();
                        if (kp == kuCaller) {
                            continue;
                        }
                        kp.sendCommand(12, tmpbb.array(), tmpbb.position());
                    }
                }

                //kuCaller.close();
                System.out.println("C012 send: Player disconnected.");
                //System.out.println("Last query: " + sQuery.toString());
                return true;
            //Broadcast "remove me" ver.b
            //13 comes from the server, not a client and is ment to be "passed along"
            //13: -> Removes player because of a map change (ID:int) (to client)
            case 13:

                kmUserMap = kcCurrent.getCaller().getMap();
                players = kmUserMap.getPlayers();
                synchronized (players) {

                    ByteBuffer tmpbb = ByteBuffer.allocate(16);
                    tmpbb.putInt(kuCaller.getID());

                    Iterator<KamiUser> itrKU = players.iterator();
                    KamiUser kp;
                    while (itrKU.hasNext()) {
                        kp = itrKU.next();
                        if (kp == kuCaller) {
                            continue;
                        }
                        kp.sendCommand(13, tmpbb.array(), tmpbb.position());
                    }
                }

                kmUserMap.removePlayer(kuCaller);
                System.out.println("Command send: 13");
                //System.out.println("Last query: "+sQuery.toString());
                return true;


            // direction change
            //20: <- Sets player movement direction (dir:Byte, time:Long) (to server)
            case 20:

                if (!kuCaller.isReadyToMove()) {
                    return false;		//System.out.println( "Time. Send:"+kcCurrent.get(1)+" server: "+(System.currentTimeMillis()-kuCaller.getTime()+1000) );
                }
                if (((Integer) kcCurrent.get(1)) > System.currentTimeMillis() - kuCaller.getTime() + 1000) {
                    System.out.println("Time mismach on user: " + kuCaller.getName());
                    return true;
                }
                kuCaller.setTime(System.currentTimeMillis());
                //System.out.println(" User \"" + kuCaller.getName()+"\" changed dir to: "+kuCaller.getDir());

                int nMoveTime = ((Integer) kcCurrent.get(1)) / KamiProcessor.nTimerTick;

                //12   11    10
                //   4  3  2
                //13 5  0  1  9
                //   6  7  8
                //14   15    16
                switch (kuCaller.getDir()) {
                    case 9:
                        kuCaller.setVectors(KamiProcessor.nMoveSpeed * (nMoveTime), 0);
                        break;
                    case 10:
                        kuCaller.setVectors(KamiProcessor.nMoveSpeed * (nMoveTime), -KamiProcessor.nMoveSpeed * (nMoveTime));
                        break;
                    case 11:
                        kuCaller.setVectors(0, -KamiProcessor.nMoveSpeed * (nMoveTime));
                        break;
                    case 12:
                        kuCaller.setVectors(-KamiProcessor.nMoveSpeed * (nMoveTime), -KamiProcessor.nMoveSpeed * (nMoveTime));
                        break;
                    case 13:
                        kuCaller.setVectors(-KamiProcessor.nMoveSpeed * (nMoveTime), 0);
                        break;
                    case 14:
                        kuCaller.setVectors(-KamiProcessor.nMoveSpeed * (nMoveTime), KamiProcessor.nMoveSpeed * (nMoveTime));
                        break;
                    case 15:
                        kuCaller.setVectors(0, KamiProcessor.nMoveSpeed * (nMoveTime));
                        break;
                    case 16:
                        kuCaller.setVectors(KamiProcessor.nMoveSpeed * (nMoveTime), KamiProcessor.nMoveSpeed * (nMoveTime));
                        break;

                }
                kuCaller.setDir((Byte) kcCurrent.get(0));
                kuCaller.move();

                //if(kuCaller.getDir()<9) {
                bb = ByteBuffer.allocate(16);
                //bb.putInt(kuCaller.getID());
                bb.putShort(kuCaller.getX());
                bb.putShort(kuCaller.getY());

                kuCaller.sendCommand(22, bb.array(), bb.position());
                //}

                //System.out.println("Command send: 21 & 22");
                return true;

            // Location accepted
            // 23: <- New location accepted () (to server)
            case 23:
                kmUserMap = kcCurrent.getCaller().getMap();
                players = kmUserMap.getPlayers();
                synchronized (players) {

                    bb = ByteBuffer.allocate(32);
                    bb.putInt(kuCaller.getID());
                    bb.put(kuCaller.getDir());
                    bb.putShort(kuCaller.getX());
                    bb.putShort(kuCaller.getY());

                    Iterator<KamiUser> itrKU = players.iterator();
                    KamiUser kp;
                    while (itrKU.hasNext()) {
                        kp = itrKU.next();
                        if (kp == kuCaller) {
                            continue;
                        }
                        kp.sendCommand(21, bb.array(), bb.position());
                    }
                }
                kuCaller.setVectors(0, 0);
                return true;
            // Location suggested
            //24: <- New location sugessted (x:Int, y:Int) (to server)
            case 24:
                kuCaller.checkAndSetLocation((Short) kcCurrent.get(0), (Short) kcCurrent.get(1));

                kmUserMap = kcCurrent.getCaller().getMap();
                players = kmUserMap.getPlayers();
                synchronized (players) {

                    bb = ByteBuffer.allocate(32);
                    bb.putInt(kuCaller.getID());
                    bb.put(kuCaller.getDir());
                    bb.putShort(kuCaller.getX());
                    bb.putShort(kuCaller.getY());

                    Iterator<KamiUser> itrKU = players.iterator();
                    KamiUser kp;
                    while (itrKU.hasNext()) {
                        kp = itrKU.next();
                        if (kp == kuCaller) {
                            continue;
                        }
                        kp.sendCommand(21, bb.array(), bb.position());
                    }
                }
                kuCaller.setVectors(0, 0);
                return true;
            // Location reset request
            //25: <- Location reset kthxbye () (to server)
            case 25:
                kc.setID(13);
                parse(kc);

                bb = ByteBuffer.allocate(2048);
                bb.put(getUTF(sDefaultMap));
                bb.putShort((short) 500);
                bb.putShort((short) 500);
                kuCaller.sendCommand(4, bb.array(), bb.position());

                kuCaller.setLocation((short) 500, (short) 500);
                moveUserTo(sDefaultMap, kuCaller);

                return true;

            // Location reset request
            //26: <- Move to "home" () (to server)
            case 26:
                kc.setID(13);
                parse(kc);

                sQuery.append(kuCaller.getUserName().charAt(0));
                sQuery.append("/");
                sQuery.append(kuCaller.getUserName());
                sQuery.append("/default.map");
                sQuery.replace(0, 255, sQuery.toString().toLowerCase());

                bb = ByteBuffer.allocate(2048);
                bb.put(getUTF(sQuery.toString()));
                bb.putShort((short) 500);
                bb.putShort((short) 500);
                kuCaller.sendCommand(4, bb.array(), bb.position());

                kuCaller.setLocation((short) 500, (short) 500);
                moveUserTo(sQuery.toString(), kuCaller);
                return true;

            // reset move time
            //27: <- Reset move time () (to server)
            case 27:
                kuCaller.setTime(System.currentTimeMillis());
                return true;


            // Broadcasting chat message
            //30: <- Sends chat message (s:String) (to server)
            case 30:
                kmUserMap = kcCurrent.getCaller().getMap();
                players = kmUserMap.getPlayers();
                synchronized (players) {

                    bb = ByteBuffer.allocate(1024);
                    bb.putInt(kuCaller.getID());
                    bb.put(getUTF(kuCaller.getName()));
                    bb.put(getUTF((String) kcCurrent.get(0)));

                    Iterator<KamiUser> itrKU = players.iterator();
                    KamiUser kp;
                    while (itrKU.hasNext()) {
                        kp = itrKU.next();
                        kp.sendCommand(31, bb.array(), bb.position());
                    }
                }

                //System.out.println("Command send: 31");
                return true;

            // Editor map recheck request
            case 100:
                KamiServer.checkMap( (String)kcCurrent.get(0) );
                kuCaller.sendID(101);

                // resending update notice to players in the game
                kmUserMap = hmLevels.get( (String)kcCurrent.get(0) );
                players = kmUserMap.getPlayers();
                Iterator<KamiUser> itrKU = players.iterator();
                KamiUser kp;
                while (itrKU.hasNext()) {
                    kp = itrKU.next();
                    kp.sendID(101);
                }
                
                return true;

            // FLASH RESERVED COMMAND - Sending: "<?xml version=\"1.0\"?><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>"
            case 1014001516:
                //os.write( (((String)kc_comm.get(0))+"\u0000").getBytes() );
                kuCaller.sendRaw((((String) kc.get(0)) + "\u0000").getBytes());
                return true;
        }
        return false;
    }

    public static synchronized void getCommand(int id, KamiCommand kc, java.io.DataInputStream is) throws IOException {

        kc.clear();
        kc.setID(id);

        switch (id) {

            //logon
            case 1:
                kc.set(0, is.readInt());
                kc.set(1, is.readByte());
                kc.set(2, is.readUTF());
                kc.set(3, is.readUTF());
                //System.out.println("Command recived: " + kc);
                break;
            //link follow
            case 3:
                kc.set(0, is.readInt());
                kc.set(1, is.readShort());
                kc.set(2, is.readShort());
                //System.out.println("Command recived: " + kc);
                break;

            //amount of players
            case 10: //empty
                //System.out.println("Command recived: " + kc);
                break;

            // direction change
            case 20:
                kc.set(0, is.readByte());
                kc.set(1, is.readInt());
                //System.out.print("Command recived: " + kc);
                break;

            // location accept
            case 23: //empty
                break;
            // location suggestion
            case 24:
                kc.set(0, is.readShort());
                kc.set(1, is.readShort());
                break;
            // location reset
            case 25: //empty
                break;
            // move to home
            case 26: //empty
                break;
            // reset move time
            case 27: //empty
                break;

            // chat message
            case 30:
                kc.set(0, is.readUTF());
                //System.out.println("Command recived: " + kc);
                break;
            // global chat message
            case 35:
                kc.set(0, is.readUTF());
                //System.out.println("Command recived: " + kc);
                break;

            // Editor map recheck request
            case 100:
                kc.set(0, is.readUTF());
                break;

            // FLASH RESERVED COMMAND "<policy-file-request/>"
            case 1014001516:

                int codePoint = -1;
                while (codePoint != 0) {
                    codePoint = is.read();
                }
                kc.set(0, "<?xml version=\"1.0\"?><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>");
                break;

            //unknow command
            default:
                System.out.println("Unknow command: " + id);
        }

    }
    // =========================================================================
    // Other
    //

    public void setStatement(Statement s) {
        sState = s;
    }

    public synchronized void addUser(KamiUser ku) {
        alPlayers.add(ku);
    }

    public synchronized void removeUser(KamiUser ku) {
        alPlayers.remove(ku);
        KamiServer.removeUser(ku);
        System.out.println("User disconnected. Current amount: " + KamiServer.countUsers());
    }
    // for processor selection

    public synchronized int getAmount() {
        return alPlayers.size();
    }

    public void addCommand(KamiCommand kc) {
        alCommands.add(kc.clone());
    }

    private void moveUserTo(String s, KamiUser kp) {
        synchronized (HashMap.class) {
            if( hmLevels.get(s)==null ) {
                //user is on a map that has been removed
                kp.setMap(hmLevels.get(sDefaultMap));
            } else {
                kp.setMap(hmLevels.get(s));
            }
        }
    }
    /*
    private void removeUserFrom(String s, KamiUser kp) {
    synchronized (HashMap.class) {
    hmLevels.get(s).removePlayer(kp);
    }
    }
     */
    // =========================================================================
    // Misc.
    //

    private byte[] getUTF(String str) throws IOException {
        int strlen = str.length();
        int utflen = 0;
        int c, count = 0;

        for (int i = 0; i < strlen; i++) {
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                utflen++;
            } else if (c > 0x07FF) {
                utflen += 3;
            } else {
                utflen += 2;
            }
        }

        if (utflen > 65535) {
            throw new java.io.UTFDataFormatException(
                    "encoded string too long: " + utflen + " bytes");
        }
        byte[] bytearr = new byte[utflen + 2];

        bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
        bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);

        int i = 0;
        for (i = 0; i < strlen; i++) {
            c = str.charAt(i);
            if (!((c >= 0x0001) && (c <= 0x007F))) {
                break;
            }
            bytearr[count++] = (byte) c;
        }

        for (; i < strlen; i++) {
            c = str.charAt(i);
            if ((c >= 0x0001) && (c <= 0x007F)) {
                bytearr[count++] = (byte) c;

            } else if (c > 0x07FF) {
                bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
                bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F));
                bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
            } else {
                bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
                bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
            }
        }

        return bytearr;
    }
}
