package Localization;

import android.util.Log;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.net.Socket;
import java.security.KeyPair;
import java.util.ArrayList;

import Localization.structs.SendLocalizationData;
import Localization.structs.LocalizationResult;
import Localization.structs.SendTrainingData;
import security.elgamal.ElGamalCipher;
import security.elgamal.ElGamal_Ciphertext;
import ui.MainActivity;
import ui.TrainActivity;

import static Localization.KeyMaster.e_sk;
import static Localization.KeyMaster.e_pk;
import static ui.MainActivity.SQLDatabase;
import static ui.MainActivity.portNumber;

import security.DGK.DGKOperations;
import security.DGK.DGKPrivateKey;
import security.DGK.DGKPublicKey;
import security.paillier.PaillierCipher;
import security.paillier.PaillierPrivateKey;
import security.paillier.PaillierPublicKey;
import security.socialistmillionaire.bob;

public class ClientThread implements Runnable
{
    private final static String TAG = "CLIENT_THREAD";

    private ObjectOutputStream toServer = null;
    private ObjectInputStream fromServer = null;

    //Pass Data back to class by reference...
    private background findMe;
    private TrainActivity trainMe;
    private background getColumns;

    //Data Objects
    private SendTrainingData sendTraining;     //Training Data
    private SendLocalizationData transmission;  //For Encrypted Paillier/DGK Transmission

    // Have all Keys in case comparison is needed!!
    private DGKPublicKey pubKey = KeyMaster.DGKpk;
    private DGKPrivateKey privKey = KeyMaster.DGKsk;
    private PaillierPublicKey pk = KeyMaster.pk;
    private PaillierPrivateKey sk = KeyMaster.sk;

    private LOCALIZATION_SCHEME LOCALIZATIONSCHEME;
    private Socket clientSocket;

    // Get all currently trained points
    public ClientThread(TrainActivity trainActivity)
    {
        this.LOCALIZATIONSCHEME = LOCALIZATION_SCHEME.from_int(-3);
        this.trainMe = trainActivity;
    }

    /*
    Called from MainActivity
    Purpose: Force mySQL Database to process data from
    training data.
     */
    public ClientThread()
    {
        this.LOCALIZATIONSCHEME = LOCALIZATION_SCHEME.from_int(-2);
    }

    ClientThread(background needMACs)
    {
        this.LOCALIZATIONSCHEME = LOCALIZATION_SCHEME.from_int(-1);
        this.getColumns = needMACs;
    }

    // Send Training Data
    public ClientThread (SendTrainingData in)
    {
        this.LOCALIZATIONSCHEME = LOCALIZATION_SCHEME.from_int(0);
        this.sendTraining = in;
    }

    // For Localization
    ClientThread (SendLocalizationData input, background search, int local)
    {
        this.findMe = search;
        this.transmission = input;
        this.LOCALIZATIONSCHEME = LOCALIZATION_SCHEME.from_int(local);
    }

    //===============================SOCKET METHODS/RUN THREAD============================================

    public void run ()
    {
        Object in;
        try
        {
            clientSocket = new Socket(SQLDatabase, portNumber);
            // Prepare I/O Stream
            this.toServer = new ObjectOutputStream(clientSocket.getOutputStream());
            this.fromServer = new ObjectInputStream(clientSocket.getInputStream());
            Log.d(TAG, "I/O Streams set!");

            switch(LOCALIZATIONSCHEME)
            {
                case GETXY:
                    toServer.writeObject("Acquire all current training points");
                    toServer.flush();

                    // Send the Map Name as well
                    toServer.writeObject(KeyMaster.map_name);
                    toServer.flush();

                    // Following this patch the server needs to know the Phone as well
                    if(MainActivity.multi_phone)
                    {
                        String [] phone_data = MainActivity.getPhoneData();
                        for (String s: phone_data)
                        {
                            Log.d(TAG, s);
                        }
                        toServer.writeObject(phone_data);
                        toServer.flush();
                    }

                    in = fromServer.readObject();
                    if(in instanceof Double [])
                    {
                        trainMe.existingX = (Double []) in;
                    }
                    else
                    {
                        Log.d(TAG, "Data Type: -3, DIDN'T GET DOUBLE X []");
                    }

                    in = fromServer.readObject();
                    if(in instanceof Double[])
                    {
                        trainMe.existingY = (Double []) in;
                    }
                    else
                    {
                        Log.d(TAG, "Data Type: -3, DIDN'T GET DOUBLE Y []");
                    }
                    break;

                /*
                Input: Command to force Database to compute Lookup Table
                Return: NOTHING. Kill Thread after
                 */
                case PROCESS:
                    toServer.writeObject("Process LUT");
                    toServer.flush();
                    if (fromServer.readBoolean())
                    {
                        Log.d(TAG, "Successfully Processed Lookup Table!");
                        MainActivity.process_good.show();
                    }
                    else
                    {
                        Log.d(TAG, "Error Processing Lookup Tables!");
                        MainActivity.process_bad.show();
                    }
                    break;
                case GET_COLUMN:
                    toServer.writeObject("Get Lookup Columns");

                    // Send the Map with all (x, y)
                    toServer.writeObject(KeyMaster.map_name);
                    toServer.flush();

                    in = fromServer.readObject();
                    if (in instanceof String [])
                    {
                        getColumns.CommonMAC = (String[]) in;
                    }
                    else
                    {
                        Log.d(TAG, "INVALID COLUMN RECEIVED! " + in.getClass());
                    }
                    break;
                case TRAIN:
                    toServer.writeObject(sendTraining);
                    toServer.flush();
                    //Wait to get confirmation that the data successfully inserted...
                    if (fromServer.readBoolean())
                    {
                        MainActivity.good_train.show();
                    }
                    else
                    {
                        MainActivity.bad_train.show();
                    }
                    break;

                case PLAIN_MIN:
                case PLAIN_DMA:
                case PLAIN_MCA:
                case PAILLIER_DMA:
                case PAILLIER_MCA:
                case PAILLIER_MIN:
                case DGK_MIN:
                case DGK_MCA:
                case DGK_DMA:
                case EL_GAMAL_DMA:
                case EL_GAMAL_MIN:
                case EL_GAMAL_MCA:
                    localize();
                    break;
                default:
                    Log.d(TAG, "Error at Thread run: No Valid Object was sent here");
                    break;
            }
        }
        catch (ClassCastException cce)
        {
            cce.printStackTrace();
        }
        catch (ClassNotFoundException cnf)
        {
            cnf.printStackTrace();
        }
        catch (IOException ioe)
        {
            Log.d(TAG,"CHECK IF YOU ARE CONNECTED TO WI-FI (Most Common Issue)");
            Log.d(TAG, "MAKE SURE YOU HAVE RIGHT IP ADDRESS!!!");
            Log.d(TAG, "IF YOU ARE STILL TIMING OUT, IT IS YOUR FIREWALL!");
            ioe.printStackTrace();
            //This can be caused if the phone is too far from 3rd floor of EC.
        }
    }

    private void localize() throws IOException, ClassNotFoundException
    {
        bob andrew;
        BigInteger [] location;
        BigInteger divisor;
        Object in;

        toServer.writeObject(transmission);
        toServer.flush();

        // Server will want to have an Alice/Bob instance ready just in case
        if(e_pk == null || e_sk == null)
        {
            andrew = new bob(clientSocket, new KeyPair(pk, sk), new KeyPair(pubKey, privKey));
        }
        else
        {
            andrew = new bob(clientSocket, new KeyPair(pk, sk), new KeyPair(pubKey, privKey),
                    new KeyPair(e_pk, e_sk));
        }
        switch(LOCALIZATIONSCHEME)
        {
            case PLAIN_MIN:
            case PLAIN_MCA:
            case PLAIN_DMA:
                in = fromServer.readObject();
                if(transmission.isREU2017)
                {
                    if (in instanceof Double[])
                    {
                        findMe.coordinates = (Double[]) in;
                    }
                    else
                    {
                        Log.d(TAG, "INVALID OBJECT: " + in.getClass());
                    }
                }
                else
                {
                    if (in instanceof ArrayList<?>)
                    {
                        for (Object o: (ArrayList<?>) in)
                        {
                            if(o instanceof LocalizationResult)
                            {
                                findMe.fromServer.add((LocalizationResult) o);
                            }
                        }
                    }
                    else
                    {
                        Log.d(TAG, "ERROR WRONG OBJECT IN PLAINTEXT 2015/2017! " + in.getClass());
                    }
                }
                break;

            case DGK_MIN:
            case DGK_MCA:
            case DGK_DMA:

                if(transmission.isREU2017)
                {
                    // bob is spawned
                    andrew.setDGKMode(true);
                    // Sort to get the Minimum value OR K-Minimum
                    andrew.run();
                    in = fromServer.readObject();
                    if (LOCALIZATIONSCHEME == LOCALIZATION_SCHEME.DGK_MIN)
                    {
                        if (in instanceof BigInteger[])
                        {
                            location = (BigInteger []) in;
                            findMe.coordinates[0] = (double) DGKOperations.decrypt(privKey, location[0]);
                            findMe.coordinates[1] = (double) DGKOperations.decrypt(privKey, location[1]);
                        }
                        else
                        {
                            Log.d(TAG, "INVALID OBJECT IN DGK_MIN: " + in.getClass());
                        }
                    }
                    else
                    {
                        // If DMA, divide all matches
                        divisor = DGKOperations.decrypt((BigInteger) in, privKey);
                        toServer.writeObject(divisor);
                        toServer.flush();

                        // Bob must stay alive to divide...
                        for (int i = 0; i < MainActivity.k; i++)
                        {
                            andrew.division(divisor.longValue() * (MainActivity.k - 1));
                        }

                        // Now you can get your location
                        in = fromServer.readObject();
                        if (in instanceof BigInteger[])
                        {
                            location = (BigInteger[]) in;
                            // Divide by the factor of both server/phone
                            findMe.coordinates[0] = (double) DGKOperations.decrypt(privKey, location[0]);
                            findMe.coordinates[1] = (double) DGKOperations.decrypt(privKey, location[1]);
                            findMe.coordinates[0] = findMe.coordinates[0]/MainActivity.FACTOR;
                            findMe.coordinates[1] = findMe.coordinates[0]/MainActivity.FACTOR;
                        }
                    }
                }
                else
                {
                    // REU 2015 DGK Code
                    in = fromServer.readObject();
                    Log.d(TAG, "DGK REU 2015");
                    if (in instanceof ArrayList<?>)
                    {
                        for (Object o: (ArrayList<?>) in)
                        {
                            Log.d(TAG, "OBJECT FOUND");
                            if(o instanceof LocalizationResult)
                            {
                                findMe.fromServer.add((LocalizationResult) o);
                            }
                        }
                    }
                    else
                    {
                        Log.d(TAG, "Output Loop: INVALID OBJECT SENT: " + in.getClass());
                    }
                }
                break;
            case PAILLIER_MIN:
            case PAILLIER_MCA:
            case PAILLIER_DMA:

                if(transmission.isREU2017)
                {
                    andrew.setDGKMode(false);
                    andrew.run();
                    in = fromServer.readObject();
                    if(LOCALIZATIONSCHEME == LOCALIZATION_SCHEME.PAILLIER_MIN)
                    {
                        if (in instanceof BigInteger [])
                        {
                            // Will always be DGK encrypted!
                            location = (BigInteger[]) in;
                            findMe.coordinates[0] = PaillierCipher.decrypt(location[0], sk).doubleValue();
                            findMe.coordinates[1] = PaillierCipher.decrypt(location[1], sk).doubleValue();
                        }
                        else
                        {
                            Log.d(TAG, "ERROR, INVALID OBJECT " + in.getClass());
                        }
                    }
                    else
                    {
                        divisor = PaillierCipher.decrypt((BigInteger) in, sk);
                        toServer.writeObject(divisor);
                        toServer.flush();

                        for (int i = 0; i < MainActivity.k; i++)
                        {
                            andrew.division(divisor.longValue() * (MainActivity.k - 1));
                        }

                        // Now you can get your location
                        in = fromServer.readObject();
                        if (in instanceof BigInteger[])
                        {
                            location = (BigInteger[]) in;
                            // Decrypt and Divide by the factor of both server/phone
                            findMe.coordinates[0] = PaillierCipher.decrypt(location[0], sk).doubleValue()/MainActivity.FACTOR;
                            findMe.coordinates[1] = PaillierCipher.decrypt(location[1], sk).doubleValue()/MainActivity.FACTOR;
                        }
                    }
                }
                else
                {
                    // REU 2015
                    in = fromServer.readObject();
                    if(in instanceof ArrayList<?>)
                    {
                        for (Object o: (ArrayList<?>) in)
                        {
                            if(o instanceof LocalizationResult)
                            {
                                findMe.fromServer .add((LocalizationResult) o);
                            }
                            else
                            {
                                throw new IllegalArgumentException("EXPECTED LOCALIZATION RESULT");
                            }
                        }
                    }
                    else
                    {
                        throw new IllegalArgumentException("INVALID OBJECT! " + in.getClass());
                    }
                }
            case EL_GAMAL_MIN:
            case EL_GAMAL_MCA:
            case EL_GAMAL_DMA:

                if(transmission.isREU2017)
                {
                    // TODO: IF NUMBER IS BIGGER THAN U?
                    andrew.repeat_ElGamal_Protocol4();

                    in = fromServer.readObject();
                    if(LOCALIZATIONSCHEME == LOCALIZATION_SCHEME.EL_GAMAL_MIN)
                    {
                        if (in instanceof ElGamal_Ciphertext[])
                        {
                            ElGamal_Ciphertext [] e_location = (ElGamal_Ciphertext[]) in;
                            findMe.coordinates[0] = ElGamalCipher.decrypt(e_sk, e_location[0]).doubleValue();
                            findMe.coordinates[1] = ElGamalCipher.decrypt(e_sk, e_location[1]).doubleValue();
                        }
                        else
                        {
                            Log.d(TAG, "ERROR, INVALID OBJECT " + in.getClass());
                        }
                    }
                    else
                    {
                        divisor = ElGamalCipher.decrypt(e_sk, (ElGamal_Ciphertext) in);
                        toServer.writeObject(divisor);
                        toServer.flush();

                        for (int i = 0; i < MainActivity.k; i++)
                        {
                            andrew.ElGamal_division(divisor.longValue() * (MainActivity.k - 1));
                        }

                        // Now you can get your location
                        in = fromServer.readObject();
                        if (in instanceof ElGamal_Ciphertext[])
                        {
                            ElGamal_Ciphertext [] e_location = (ElGamal_Ciphertext[]) in;
                            // Decrypt and divide by the factor of both server/phone
                            findMe.coordinates[0] = ElGamalCipher.decrypt(e_sk, e_location[0]).doubleValue()/MainActivity.FACTOR;
                            findMe.coordinates[1] = ElGamalCipher.decrypt(e_sk, e_location[1]).doubleValue()/MainActivity.FACTOR;
                        }
                    }
                }
                else
                {
                    // REU 2015
                    in = fromServer.readObject();
                    if(in instanceof ArrayList<?>)
                    {
                        for (Object o: (ArrayList<?>) in)
                        {
                            if(o instanceof LocalizationResult)
                            {
                                findMe.fromServer .add((LocalizationResult) o);
                            }
                            else
                            {
                                throw new IllegalArgumentException("EXPECTED LOCALIZATION RESULT");
                            }
                        }
                    }
                    else
                    {
                        throw new IllegalArgumentException("INVALID OBJECT! " + in.getClass());
                    }
                }
        }
    }
}