/* Copyright 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.mobilyzer;


import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.os.AsyncTask;

import org.apache.http.HttpVersion;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.mobilyzer.gcm.GCMManager;
import com.mobilyzer.util.Logger;
import com.mobilyzer.util.MeasurementJsonConvertor;
import com.mobilyzer.util.PhoneUtils;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/**
 * Handles checkins with the server.
 */
public class Checkin {
  private static final int POST_TIMEOUT_MILLISEC = 20 * 1000;
  private Context context;
  private Date lastCheckin;
  private volatile Cookie authCookie = null;
  private AccountSelector accountSelector = null;
  PhoneUtils phoneUtils;
  String gcm_registraion_id;
  
  public Checkin(Context context) {
    phoneUtils = PhoneUtils.getPhoneUtils();
    this.context = context;
    this.gcm_registraion_id="";
  }

  /** Shuts down the checkin thread */
  public void shutDown() {
    if (this.accountSelector != null) {
      this.accountSelector.shutDown();
    }
  }
  
  /** Return a fake authentication cookie for a test server instance */
  private Cookie getFakeAuthCookie() {
    BasicClientCookie cookie = new BasicClientCookie(
        "dev_appserver_login",
        "[email protected]:False:185804764220139124118");
    cookie.setDomain(".google.com");
    cookie.setVersion(1);
    cookie.setPath("/");
    cookie.setSecure(false);
    return cookie;
  }
  
  public Date lastCheckinTime() {
    return this.lastCheckin;
  }
  
  public List<MeasurementTask> checkin(ResourceCapManager resourceCapManager, GCMManager gcm) throws IOException {
    Logger.i("Checkin.checkin() called");
    boolean checkinSuccess = false;
    gcm_registraion_id=gcm.getRegistrationId();
    try {
      JSONObject status = new JSONObject();
      DeviceInfo info = phoneUtils.getDeviceInfo();
      // TODO(Wenjie): There is duplicated info here, such as device ID. 
      status.put("id", info.deviceId);
      status.put("manufacturer", info.manufacturer);
      status.put("model", info.model);
      status.put("os", info.os);
      /**
       * TODO: checkin task don't belongs to any app. So we just fill
       * request_app field with server task key   
       */
      
      DeviceProperty deviceProperty=phoneUtils.getDeviceProperty(Config.CHECKIN_KEY);
      deviceProperty.setRegistrationId(gcm.getRegistrationId());
      Logger.d("Checkin-> GCMManager: "+gcm.getRegistrationId());
      
      status.put("properties", MeasurementJsonConvertor.encodeToJson(deviceProperty));
      
      if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) {
    	  resourceCapManager.updateDataUsage(ResourceCapManager.PHONEUTILCOST);
      }
      
      Logger.d(status.toString());
      
      Logger.d("Checkin: "+status.toString());
      
      
      String result = serviceRequest("checkin", status.toString());
      Logger.d("Checkin result: " + result);
      if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) {
    	  resourceCapManager.updateDataUsage(result.length());
      }
      
      // Parse the result
      Vector<MeasurementTask> schedule = new Vector<MeasurementTask>();
      JSONArray jsonArray = new JSONArray(result);
      

      for (int i = 0; i < jsonArray.length(); i++) {
        Logger.d("Parsing index " + i);
        JSONObject json = jsonArray.optJSONObject(i);
        Logger.d("Value is " + json);
        // checkin task must support 
        if (json != null && 
            MeasurementTask.getMeasurementTypes().contains(json.get("type"))) {
          try {
            MeasurementTask task = 
                MeasurementJsonConvertor.makeMeasurementTaskFromJson(json);
            Logger.i(MeasurementJsonConvertor.toJsonString(task.measurementDesc));
            
            schedule.add(task);
          } catch (IllegalArgumentException e) {
            Logger.w("Could not create task from JSON: " + e);
            // Just skip it, and try the next one
          }
        }
      }
      
      this.lastCheckin = new Date();
      Logger.i("Checkin complete, got " + schedule.size() +
          " new tasks");
      checkinSuccess = true;
      return schedule;
    } catch (JSONException e) {
      Logger.e("Got exception during checkin", e);
      throw new IOException("There is exception during checkin()");
    } catch (IOException e) {
      Logger.e("Got exception during checkin", e);
      throw e;
    } finally {
      if (!checkinSuccess) {
        // Failure probably due to authToken expiration. Will authenticate upon next checkin.
        this.accountSelector.setAuthImmediately(true);
        this.authCookie = null;
      }
    }
  }


  /**
   * Read in the results of tasks completed to date from a file, then clear the file.
   * 
   * @return The results as a JSONArray, ready for sending to the server.
   */
  private synchronized JSONArray readResultsFromFile() {

    JSONArray results = new JSONArray();
    try {
      Logger.d("Loading results from disk: "+context.getFilesDir());
      
      FileInputStream inputstream = context.openFileInput("results");
      InputStreamReader streamreader = new InputStreamReader(inputstream);
      BufferedReader bufferedreader = new BufferedReader(streamreader);

      String line;
      int count = 0;
      while ((line = bufferedreader.readLine()) != null) {
        JSONObject jsonTask;
        try {
          jsonTask = new JSONObject(line);
          count++;
          results.put(jsonTask);
        } catch (JSONException e) {
          Logger.e("", e);
        }
      }
      Logger.i("Got " + count + " results from file");

      bufferedreader.close();
      streamreader.close();
      inputstream.close();

      // delete file once done, to avoid uploading results twice
      context.deleteFile("results");


    } catch (FileNotFoundException e) {
      Logger.e("", e);
    } catch (IOException e) {
      Logger.e("", e);
    }
    return results;
  }
  
  public void uploadMeasurementResult(Vector<MeasurementResult> finishedTasks, ResourceCapManager resourceCapManager)
      throws IOException {
    JSONArray resultArray = readResultsFromFile();
    for (MeasurementResult result : finishedTasks) {
      try {
        resultArray.put(MeasurementJsonConvertor.encodeToJson(result));
      } catch (JSONException e1) {
        Logger.e("Error when adding " + result);
      }
    }
    
    JSONArray chunckedArray= new JSONArray();
    int i=0;
    for (;i<resultArray.length();i++){
      try {
        chunckedArray.put(resultArray.getJSONObject(i));
      } catch (JSONException e) {
        Logger.e("Error when adding index " +i + " to array");
      }
      
      if((i+1)%100==0){
        Logger.d("uploading "+chunckedArray.length()+" measurements");
        uploadChunkedArray(chunckedArray, resourceCapManager);
        chunckedArray= new  JSONArray();
      }
    }
    if(i%100!=0){
      Logger.d("uploading "+chunckedArray.length()+" measurements");
      uploadChunkedArray(chunckedArray, resourceCapManager);
    }
    Logger.i("TaskSchedule.uploadMeasurementResult() complete");
    
  }
  
  
  private  void uploadChunkedArray(JSONArray resultArray, ResourceCapManager resourceCapManager)
      throws IOException {
    Logger.i("uploadChunkedArray uploading: " + 
        resultArray.toString());
    if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) {
    	resourceCapManager.updateDataUsage(resultArray.toString().length());
    }
    String response = serviceRequest("postmeasurement", resultArray.toString());
    try {
      JSONObject responseJson = new JSONObject(response);
      if (!responseJson.getBoolean("success")) {
        throw new IOException("Failure posting measurement result");
      }
    } catch (JSONException e) {
      throw new IOException(e.getMessage());
    }
  }
  
  
  class NotUIBlockingResultUploader extends AsyncTask<String , Void, String> {

	@Override
	protected String doInBackground(String... results) {
		if(results.length!=1){
			return "";
		}
		String r=results[0];
		String response="";
		try {
			response=serviceRequest("postmeasurement", r);
		} catch (IOException e) {
			Logger.e("Failed to upload local event: "+e.getMessage());
		}
		return response;
	}
  }
  
  
  public void uploadSingleMeasurementResult(MeasurementResult result, ResourceCapManager resourceCapManager)
      throws IOException, InterruptedException, ExecutionException {
    
    result.getDeviceProperty().registrationId=gcm_registraion_id;
    try {
      JSONArray resultArray= new JSONArray();
      resultArray.put(MeasurementJsonConvertor.encodeToJson(result));
      Logger.d("Single Measurement result converted to json: "+resultArray.toString());
      if (PhoneUtils.getPhoneUtils().getNetwork() != PhoneUtils.NETWORK_WIFI) {
    	  resourceCapManager.updateDataUsage(resultArray.toString().length());
      }
      
      String response=new NotUIBlockingResultUploader().execute(resultArray.toString()).get();
//      String response = serviceRequest("postmeasurement", resultJson.toString());
      try {
        JSONObject responseJson = new JSONObject(response);
        if (!responseJson.getBoolean("success")) {
          throw new IOException("Failure posting single measurement result");
        }
      } catch (JSONException e) {
        throw new IOException(e.getMessage());
      }
    } catch (JSONException e1) {
      Logger.d("TaskSchedule.uploadSingleMeasurementResult() complete");
    }
    Logger.d("TaskSchedule.uploadSingleMeasurementResult() complete");
    
  }
  
  
  /**
   * Used to generate SSL sockets.
   */
  class MySSLSocketFactory extends SSLSocketFactory {
    SSLContext sslContext = SSLContext.getInstance("TLS");

    public MySSLSocketFactory(KeyStore truststore)
        throws NoSuchAlgorithmException, KeyManagementException,
        KeyStoreException, UnrecoverableKeyException {
      super(truststore);

      X509TrustManager tm = new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() {
          return null;
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
          // Do nothing
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
          // Do nothing
        }
      };

      sslContext.init(null, new TrustManager[] { tm }, null);
    }

    @Override
    public Socket createSocket(Socket socket, String host, int port,
        boolean autoClose) throws IOException, UnknownHostException {
      return sslContext.getSocketFactory().createSocket(socket, host, port,
          autoClose);
    }

    @Override
    public Socket createSocket() throws IOException {
      return sslContext.getSocketFactory().createSocket();
    }
  }

  /**
   * Return an appropriately-configured HTTP client.
   */
  private HttpClient getNewHttpClient() {
    DefaultHttpClient client;
    try {
      KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
      trustStore.load(null, null);

      SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
      sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

      HttpParams params = new BasicHttpParams();
      HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
      HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);
      
      HttpConnectionParams.setConnectionTimeout(params, POST_TIMEOUT_MILLISEC);
      HttpConnectionParams.setSoTimeout(params, POST_TIMEOUT_MILLISEC);

      SchemeRegistry registry = new SchemeRegistry();
      registry.register(new Scheme("http", PlainSocketFactory
          .getSocketFactory(), 80));
      registry.register(new Scheme("https", sf, 443));

      ClientConnectionManager ccm = new ThreadSafeClientConnManager(params,
          registry);
      client = new DefaultHttpClient(ccm, params);
    } catch (Exception e) {
      Logger.w("Unable to create SSL HTTP client", e);
      client = new DefaultHttpClient();
    }
    
    // TODO(mdw): For some reason this is not sending the cookie to the
    // test server, probably because the cookie itself is not properly
    // initialized. Below I manually set the Cookie header instead.
    CookieStore store = new BasicCookieStore();
    store.addCookie(authCookie);
    client.setCookieStore(store);
    return client;
  }
  
  public String serviceRequest(String url, String jsonString) 
      throws IOException {
    
    if (this.accountSelector == null) {
      accountSelector = new AccountSelector(context);
    }
    if (!accountSelector.isAnonymous()) {
      synchronized (this) {
        if (authCookie == null) {
          if (!checkGetCookie()) {
            throw new IOException("No authCookie yet");
          }
        }
      }
    }
    
    HttpClient client = getNewHttpClient();
    String fullurl = (accountSelector.isAnonymous() ?
                      phoneUtils.getAnonymousServerUrl() :
                      phoneUtils.getServerUrl()) + "/" + url;
    Logger.i("Checking in to " + fullurl);
    HttpPost postMethod = new HttpPost(fullurl);
    
    StringEntity se;
    try {
      se = new StringEntity(jsonString);
    } catch (UnsupportedEncodingException e) {
      throw new IOException(e.getMessage());
    }
    postMethod.setEntity(se);
    postMethod.setHeader("Accept", "application/json");
    postMethod.setHeader("Content-type", "application/json");
    if (!accountSelector.isAnonymous()) {
      // TODO(mdw): This should not be needed
      postMethod.setHeader("Cookie", authCookie.getName() + "=" + authCookie.getValue());
    }

    ResponseHandler<String> responseHandler = new BasicResponseHandler();
    Logger.i("Sending request: " + fullurl);
    String result = client.execute(postMethod, responseHandler);
    return result;
  }
  
  /**
   * Initiates the process to get the authentication cookie for the user account.
   * Returns immediately.
   */
  public synchronized void getCookie() {
    if (phoneUtils.isTestingServer(phoneUtils.getServerUrl())) {
      Logger.i("Setting fakeAuthCookie");
      authCookie = getFakeAuthCookie();
      return;
    }
    if (this.accountSelector == null) {
      accountSelector = new AccountSelector(context);
    }
    
    try {
      // Authenticates if there are no ongoing ones
      if (accountSelector.getCheckinFuture() == null) {
        accountSelector.authenticate();
      }
    } catch (OperationCanceledException e) {
      Logger.e("Unable to get auth cookie", e);
    } catch (AuthenticatorException e) {
      Logger.e("Unable to get auth cookie", e);
    } catch (IOException e) {
      Logger.e("Unable to get auth cookie", e);
    }
  }
  
  /**
   * Resets the checkin variables in AccountSelector
   * */
  public void initializeAccountSelector() {
    accountSelector.resetCheckinFuture();
    accountSelector.setAuthImmediately(false);
  }
  
  private synchronized boolean checkGetCookie() {
    if (phoneUtils.isTestingServer(phoneUtils.getServerUrl())) {
      authCookie = getFakeAuthCookie();
      return true;
    }
    Future<Cookie> getCookieFuture = accountSelector.getCheckinFuture();
    if (getCookieFuture == null) {
      Logger.i("checkGetCookie called too early");
      return false;
    }
    if (getCookieFuture.isDone()) {
      try {
        authCookie = getCookieFuture.get();
        Logger.i("Got authCookie: " + authCookie);
        return true;
      } catch (InterruptedException e) {
        Logger.e("Unable to get auth cookie", e);
        return false;
      } catch (ExecutionException e) {
        Logger.e("Unable to get auth cookie", e);
        return false;
      }
    } else {
      Logger.i("getCookieFuture is not yet finished");
      return false;
    }
  }
  
  
}