/*
 * 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 java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashMap;

import android.os.Parcel;
import android.os.Parcelable;
import android.util.StringBuilderPrinter;

import com.mobilyzer.measurements.DnsLookupTask;
import com.mobilyzer.measurements.HttpTask;
import com.mobilyzer.measurements.ParallelTask;
import com.mobilyzer.measurements.PingTask;
import com.mobilyzer.measurements.RRCTask;
import com.mobilyzer.measurements.SequentialTask;
import com.mobilyzer.measurements.TCPThroughputTask;
import com.mobilyzer.measurements.TracerouteTask;
import com.mobilyzer.measurements.UDPBurstTask;
import com.mobilyzer.measurements.DnsLookupTask.DnsLookupDesc;
import com.mobilyzer.measurements.HttpTask.HttpDesc;
import com.mobilyzer.measurements.PingTask.PingDesc;
import com.mobilyzer.measurements.TCPThroughputTask.TCPThroughputDesc;
import com.mobilyzer.measurements.TracerouteTask.TracerouteDesc;
import com.mobilyzer.measurements.UDPBurstTask.UDPBurstDesc;
import com.mobilyzer.measurements.VideoQoETask;
import com.mobilyzer.measurements.VideoQoETask.VideoQoEDesc;
import com.mobilyzer.util.Logger;
import com.mobilyzer.util.MeasurementJsonConvertor;
import com.mobilyzer.util.PhoneUtils;
import com.mobilyzer.util.Util;

/**
 * POJO that represents the result of a measurement
 * 
 * @see MeasurementDesc
 */
public class MeasurementResult implements Parcelable {

  private String deviceId;
  private DeviceProperty properties;// TODO needed for sending back the
  // results to server
  private long timestamp;
  private boolean success;
  private String type;
  private TaskProgress taskProgress;
  private MeasurementDesc parameters;
  private HashMap<String, String> values;
  private ArrayList<HashMap<String, String>> contextResults;

  public enum TaskProgress {
    COMPLETED, PAUSED, FAILED, RESCHEDULED
  }

  /**
   * @param deviceProperty DeviceProperty object which will be attached to the result
   * @param type measurement type
   * @param timestamp
   * @param taskProgress progress of the task: COMPLETED, PAUSED, FAILED
   * @param measurementDesc MeasurementDesc of the task
   */
  public MeasurementResult(String id, DeviceProperty deviceProperty, String type, long timeStamp,
                           TaskProgress taskProgress, MeasurementDesc measurementDesc) {
    super();

    this.deviceId = id;
    this.type = type;
    this.properties = deviceProperty;
    this.timestamp = timeStamp;
    this.taskProgress = taskProgress;
    if (this.taskProgress == TaskProgress.COMPLETED) {
      this.success = true;
    } else {
      this.success = false;
    }
    this.parameters = measurementDesc;
    this.parameters.parameters = measurementDesc.parameters;
    this.values = new HashMap<String, String>();
    this.contextResults = new ArrayList<HashMap<String, String>>();
  }

  public MeasurementDesc getMeasurementDesc(){
    return this.parameters;
  }

  public DeviceProperty getDeviceProperty(){
    return this.properties;
  }


  @SuppressWarnings("unchecked")
  public void addContextResults(ArrayList<HashMap<String, String>> contextResults) {
    this.contextResults = (ArrayList<HashMap<String, String>>) contextResults.clone();
  }

  private static String getStackTrace(Throwable error) {
    final Writer result = new StringWriter();
    final PrintWriter printWriter = new PrintWriter(result);
    error.printStackTrace(printWriter);
    return result.toString();
  }

  /**
   * Creates measurement result for the failed task by including the error message
   * 
   * @param task input task that failed
   * @param error that occurred during the execution of task
   * @return list of failure measurement results for created for the input task
   */
  public static MeasurementResult[] getFailureResult(MeasurementTask task, Throwable error) {
    PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
    ArrayList<MeasurementResult> results = new ArrayList<MeasurementResult>();

    if (task.getType().equals(ParallelTask.TYPE)) {
      ParallelTask pTask = (ParallelTask) task;
      MeasurementResult[] tempResults =
          MeasurementResult.getFailureResults(pTask.getTasks(), error);
      for (MeasurementResult r : tempResults) {
        results.add(r);
      }
    } else if (task.getType().equals(SequentialTask.TYPE)) {
      SequentialTask sTask = (SequentialTask) task;
      MeasurementResult[] tempResults =
          MeasurementResult.getFailureResults(sTask.getTasks(), error);
      for (MeasurementResult r : tempResults) {
        results.add(r);
      }
    } else {
      MeasurementResult r =
          new MeasurementResult(phoneUtils.getDeviceInfo().deviceId,
            phoneUtils.getDeviceProperty(task.getKey()), task.getType(),
            System.currentTimeMillis() * 1000,
            TaskProgress.FAILED, task.measurementDesc);
      Logger.e(error.toString() + "\n" + getStackTrace(error));
      r.addResult("error", error.toString());
      results.add(r);
    }
    return results.toArray(new MeasurementResult[results.size()]);
  }

  /**
   * Generating failure results for the task list in parallel and sequential
   * tasks. Not public visible
   * @param tasks task list in parallel and sequential tasks
   * @param err error that occurred during the execution of task
   * @return list of failure measurement results of the task list
   */
  private static MeasurementResult[] getFailureResults(MeasurementTask[] tasks, Throwable err) {
    ArrayList<MeasurementResult> results = new ArrayList<MeasurementResult>();
    if (tasks != null) {
      for (MeasurementTask t : tasks) {
        MeasurementResult[] tempResults = getFailureResult(t, err);
        for (MeasurementResult r : tempResults) {
          results.add(r);
        }
      }
    }
    return results.toArray(new MeasurementResult[results.size()]);
  }

  /**
   * Returns the type of this result
   */
  public String getType() {
    return parameters.getType();
  }

  /**
   * @return Task progress for this task
   */
  public TaskProgress getTaskProgress() {
    return this.taskProgress;
  }

  /**
   * @param progress new task progress to be set
   */
  public void setTaskProgress(TaskProgress progress) {
    this.taskProgress = progress;
  }

  /**
   * @return key/value pairs of measurement result
   */
  public HashMap<String,String> getValues(){
    return this.values;
  }
  
  /**
   * Check if this task is succeed
   * @return true if taskProgress equals to COMPLETED, false otherwise
   */
  public boolean isSucceed() {
    return this.taskProgress == TaskProgress.COMPLETED ? true : false; 
  }
  
  /**
   * adds a new task parameter to the list of parameters
   * @param key key for new parameter
   * @param value value for new parameter
   */
  public void setParameter(String key, String value) {
    this.parameters.parameters.put(key, value);
  }

  /**
   * @param key key for retrieving value
   * @return value for that key
   */
  public String getParameter(String key) {
    return this.parameters.parameters.get(key);
  }
  
  public void setMeasurmentDesc(MeasurementDesc desc){
	  this.parameters=desc;
  }

  /* Add the measurement results of type String into the class */
  public void addResult(String resultType, Object resultVal) {
    this.values.put(resultType, MeasurementJsonConvertor.toJsonString(resultVal));
  }

  /* Returns a string representation of the result */
  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    StringBuilderPrinter printer = new StringBuilderPrinter(builder);
    Formatter format = new Formatter();
    try {
      if (type.equals(PingTask.TYPE)) {
        getPingResult(printer, values);
      } else if (type.equals(HttpTask.TYPE)) {
        getHttpResult(printer, values);
      } else if (type.equals(DnsLookupTask.TYPE)) {
        getDnsResult(printer, values);
      } else if (type.equals(TracerouteTask.TYPE)) {
        getTracerouteResult(printer, values);
      } else if (type.equals(UDPBurstTask.TYPE)) {
        getUDPBurstResult(printer, values);
      } else if (type.equals(TCPThroughputTask.TYPE)) {
        getTCPThroughputResult(printer, values);
      } else if (type.equals(RRCTask.TYPE)){
        getRRCResult(printer, values);
      } else if (type.equals(VideoQoETask.TYPE)) {
        getVideoQoEResult(printer, values);
      }
      else {
        Logger.e("Failed to get results for unknown measurement type " + type);
      }
      return builder.toString();
    } catch (NumberFormatException e) {
      Logger.e("Exception occurs during constructing result string for user", e);
    } catch (ClassCastException e) {
      Logger.e("Exception occurs during constructing result string for user", e);
    } catch (Exception e) {
      Logger.e("Exception occurs during constructing result string for user", e);
    }
    return "Measurement has failed";
  }

  private void getPingResult(StringBuilderPrinter printer, HashMap<String, String> values) {
    PingDesc desc = (PingDesc) parameters;
    printer.println("[Ping]");
    printer.println("Target: " + desc.target);
    String ipAddress = removeQuotes(values.get("target_ip"));
    // TODO: internationalize 'Unknown'.
    if (ipAddress == null) {
      ipAddress = "Unknown";
    }
    printer.println("IP address: " + ipAddress);
    printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp));
    printIPTestResult(printer);

    if (taskProgress == TaskProgress.COMPLETED) {
      float packetLoss = Float.parseFloat(values.get("packet_loss"));
      int count = Integer.parseInt(values.get("packets_sent"));
      printer.println("\n" + count + " packets transmitted, " + (int) (count * (1 - packetLoss))
        + " received, " + (packetLoss * 100) + "% packet loss");

      float value = Float.parseFloat(values.get("mean_rtt_ms"));
      printer.println("Mean RTT: " + String.format("%.1f", value) + " ms");

      value = Float.parseFloat(values.get("min_rtt_ms"));
      printer.println("Min RTT:  " + String.format("%.1f", value) + " ms");

      value = Float.parseFloat(values.get("max_rtt_ms"));
      printer.println("Max RTT:  " + String.format("%.1f", value) + " ms");

      value = Float.parseFloat(values.get("stddev_rtt_ms"));
      printer.println("Std dev:  " + String.format("%.1f", value) + " ms");
    } else if (taskProgress == TaskProgress.PAUSED) {
      printer.println("Ping paused!");
    } else {
      printer.println("Error: " + values.get("error"));
    }
  }

  private void getHttpResult(StringBuilderPrinter printer, HashMap<String, String> values) {
    HttpDesc desc = (HttpDesc) parameters;
    printer.println("[HTTP]");
    printer.println("URL: " + desc.url);
    printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp));
    printIPTestResult(printer);

    if (taskProgress == TaskProgress.COMPLETED) {
      int headerLen = Integer.parseInt(values.get("headers_len"));
      int bodyLen = Integer.parseInt(values.get("body_len"));
      int time = Integer.parseInt(values.get("time_ms"));
      printer.println("");
      printer.println("Downloaded " + (headerLen + bodyLen) + " bytes in " + time + " ms");
      printer.println("Bandwidth: " + (headerLen + bodyLen) * 8 / time + " Kbps");
    } else if (taskProgress == TaskProgress.PAUSED) {
      printer.println("Http paused!");
    } else {
      printer.println("Http download failed, status code " + values.get("code"));
      printer.println("Error: " + values.get("error"));
    }
  }

  private void getDnsResult(StringBuilderPrinter printer, HashMap<String, String> values) {
    DnsLookupDesc desc = (DnsLookupDesc) parameters;
    printer.println("[DNS Lookup]");
    printer.println("Target: " + desc.target);
    printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp));
    printIPTestResult(printer);

    if (taskProgress == TaskProgress.COMPLETED) {
      String ipAddress = removeQuotes(values.get("address"));
      if (ipAddress == null) {
        ipAddress = "Unknown";
      }
      printer.println("\nAddress: " + ipAddress);
      int time = Integer.parseInt(values.get("time_ms"));
      printer.println("Lookup time: " + time + " ms");
    } else if (taskProgress == TaskProgress.PAUSED) {
      printer.println("DNS look up paused!");
    } else {
      printer.println("Error: " + values.get("error"));
    }
  }

  private void getTracerouteResult(StringBuilderPrinter printer, HashMap<String, String> values) {
    TracerouteDesc desc = (TracerouteDesc) parameters;
    printer.println("[Traceroute]");
    printer.println("Target: " + desc.target);
    printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp));
    printIPTestResult(printer);

    if (taskProgress == TaskProgress.COMPLETED) {
      // Manually inject a new line
      printer.println(" ");

      int hops = Integer.parseInt(values.get("num_hops"));
      int hop_str_len = String.valueOf(hops + 1).length();
      for (int i = 1; i <= hops; i++) {
        String key = "hop_" + i + "_addr_1";
        String ipAddress = removeQuotes(values.get(key));
        if (ipAddress == null) {
          ipAddress = "Unknown";
        }
        String hop_str = String.valueOf(i);
        String hopInfo = hop_str;
        for (int j = 0; j < hop_str_len + 1 - hop_str.length(); ++j) {
          hopInfo += " ";
        }
        hopInfo += ipAddress;
        // Maximum IP address length is 15.
        for (int j = 0; j < 16 - ipAddress.length(); ++j) {
          hopInfo += " ";
        }

        key = "hop_" + i + "_rtt_ms";
        // The first and last character of this string are double
        // quotes.
        String timeStr = removeQuotes(values.get(key));
        if (timeStr == null) {
          timeStr = "Unknown";
          printer.println(hopInfo + "-1 ms");
        }else{
          float time = Float.parseFloat(timeStr);
          printer.println(hopInfo + String.format("%6.2f", time) + " ms");  
        }

        
      }
    } else if (taskProgress == TaskProgress.PAUSED) {
      printer.println("Traceroute paused!");
    } else {
      printer.println("Error: " + values.get("error"));
    }
  }

  private void getUDPBurstResult(StringBuilderPrinter printer, HashMap<String, String> values) {
    UDPBurstDesc desc = (UDPBurstDesc) parameters;
    if (desc.dirUp) {
      printer.println("[UDPBurstUp]");
    } else {
      printer.println("[UDPBurstDown]");
    }
    printer.println("Target: " + desc.target);
    printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp));
    printIPTestResult(printer);

    if (taskProgress == TaskProgress.COMPLETED) {
      printer.println("IP addr: " + values.get("target_ip"));
      printIPTestResult(printer);
//      printer.println("Packet size: " + desc.packetSizeByte + "B");
//      printer.println("Number of packets to be sent: " + desc.udpBurstCount);
//      printer.println("Interval between packets: " + desc.udpInterval + "ms");

      String lossRatio = String.format("%.2f", Double.parseDouble(values.get("loss_ratio")) * 100);
      String outOfOrderRatio =
          String.format("%.2f", Double.parseDouble(values.get("out_of_order_ratio")) * 100);
      printer.println("\nLoss ratio: " + lossRatio + "%");
      printer.println("Out of order ratio: " + outOfOrderRatio + "%");
      printer.println("Jitter: " + values.get("jitter") + "ms");
    } else if (taskProgress == TaskProgress.PAUSED) {
      printer.println("UDP Burst paused!");
    } else {
      printer.println("Error: " + values.get("error"));
    }
  }

  private void getTCPThroughputResult(StringBuilderPrinter printer, HashMap<String, String> values) {
    TCPThroughputDesc desc = (TCPThroughputDesc) parameters;
    if (desc.dir_up) {
      printer.println("[TCP Uplink]");
    } else {
      printer.println("[TCP Downlink]");
    }
    printer.println("Target: " + desc.target);
    printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp));
    printIPTestResult(printer);

    if (taskProgress == TaskProgress.COMPLETED) {
      printer.println("");
      // Display result with precision up to 2 digit
      String speedInJSON = values.get("tcp_speed_results");
      String dataLimitExceedInJSON = values.get("data_limit_exceeded");
      String displayResult = "";

      double tp = desc.calMedianSpeedFromTCPThroughputOutput(speedInJSON);
      double KB = Math.pow(2, 10);
      if (tp < 0) {
        displayResult = "No results available.";
      } else if (tp > KB * KB) {
        displayResult = "Speed: " + String.format("%.2f", tp / (KB * KB)) + " Gbps";
      } else if (tp > KB) {
        displayResult = "Speed: " + String.format("%.2f", tp / KB) + " Mbps";
      } else {
        displayResult = "Speed: " + String.format("%.2f", tp) + " Kbps";
      }

      // Append notice for exceeding data limit
      if (dataLimitExceedInJSON.equals("true")) {
        displayResult +=
            "\n* Task finishes earlier due to exceeding " + "maximum number of "
                + ((desc.dir_up) ? "transmitted" : "received") + " bytes";
      }
      printer.println(displayResult);
    } else if (taskProgress == TaskProgress.PAUSED) {
      printer.println("TCP Throughput paused!");
    } else {
      printer.println("Error: " + values.get("error"));
    }
  }

  
  private void getRRCResult(StringBuilderPrinter printer, HashMap<String, String> values) {
    printer.println("[RRC Inference]");
    if (taskProgress == TaskProgress.COMPLETED) {
      printer.println("Succeed!");
    }
    else {
      printer.println("Failed!");
    }
    printer.println("Results uploaded to server");
  }
  private void getVideoQoEResult(StringBuilderPrinter printer, HashMap<String, String> values) {
	    VideoQoEDesc desc = (VideoQoEDesc) parameters;
	    printer.println("[Video QoE Measurement]");
	    printer.println("Content ID: " + desc.contentId);
	    printer.println("Streaming Algorithm: " + desc.contentType);
	    printer.println("Timestamp: " + Util.getTimeStringFromMicrosecond(properties.timestamp));
	    printIPTestResult(printer);

	    if (taskProgress == TaskProgress.COMPLETED) {
	      printer.println("");
//	      printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_IS_SUCCEED));
	      printer.println("Num of frame dropped" + ": " + values.get("video_num_frame_dropped"));
	      printer.println("Initial loading time" + ": " + values.get("video_initial_loading_time"));
//	      printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_REBUFFER_TIME + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_REBUFFER_TIME));
//	      printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_TIMESTAMP));
//	      printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_VALUE + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_GOODPUT_VALUE));
//	      printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_TIMESTAMP));
//	      printer.println(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_VALUE + ": " + values.get(UpdateIntent.VIDEO_TASK_PAYLOAD_BITRATE_VALUE));
	    } else if (taskProgress == TaskProgress.PAUSED) {
	      printer.println("Video QoE task paused!");
	    } else {
	      printer.println("Error: " + values.get("error"));
	    }
	  }

  /**
   * Removes the quotes surrounding the string. If |str| is null, returns null.
   */
  private String removeQuotes(String str) {
    return str != null ? str.replaceAll("^\"|\"", "") : null;
  }

  /**
   * Print ip connectivity and hostname resolvability result
   */
  private void printIPTestResult(StringBuilderPrinter printer) {
//    printer.println("IPv4/IPv6 Connectivity: " + properties.ipConnectivity);
//    printer.println("IPv4/IPv6 Domain Name Resolvability: " + properties.dnResolvability);
  }

  /** Necessary function for Parcelable **/
  private MeasurementResult(Parcel in) {
//    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    deviceId = in.readString();
    properties = in.readParcelable(DeviceProperty.class.getClassLoader());
    timestamp = in.readLong();
    type = in.readString();
    taskProgress = (TaskProgress) in.readSerializable();
    if (this.taskProgress == TaskProgress.COMPLETED) {
      this.success = true;
    } else {
      this.success = false;
    }
    parameters = in.readParcelable(MeasurementDesc.class.getClassLoader());
//    values = in.readHashMap(loader);
    int valuesSize = in.readInt();
    values = new HashMap<String, String>();
    for (int i = 0; i < valuesSize; i++) {
      values.put(in.readString(), in.readString());
    }
//  contextResults = in.readArrayList(loader);
    contextResults= new ArrayList<HashMap<String,String>>();
    int contextResultsSize=in.readInt();
    for (int i = 0; i < contextResultsSize; i++) {
      int contextResultsHashMapSize=in.readInt();
      HashMap<String,String> tempHashMap= new HashMap<String, String>();
      for (int j = 0; j < contextResultsHashMapSize; j++) {
        tempHashMap.put(in.readString(), in.readString());
      }
      contextResults.add(tempHashMap);
    }


  }

  public static final Parcelable.Creator<MeasurementResult> CREATOR =
      new Parcelable.Creator<MeasurementResult>() {
    public MeasurementResult createFromParcel(Parcel in) {
      return new MeasurementResult(in);
    }

    public MeasurementResult[] newArray(int size) {
      return new MeasurementResult[size];
    }
  };

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(Parcel out, int flag) {
    out.writeString(deviceId);
    out.writeParcelable(properties, flag);
    out.writeLong(timestamp);
    out.writeString(type);
    out.writeSerializable(taskProgress);
    out.writeParcelable(parameters, flag);
//    out.writeMap(values);
    out.writeInt(values.size());
    for (String s: values.keySet()) {
        out.writeString(s);
        out.writeString(values.get(s));
    }
//    out.writeList(contextResults);
    out.writeInt(contextResults.size());
    for (HashMap<String, String> map: contextResults) {
      out.writeInt(map.size());
      for(String s: map.keySet()){
        out.writeString(s);
        out.writeString(map.get(s));
      }
    }
    

  }
}