/*
 * 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 com.mobilyzer.MeasurementScheduler.DataUsageProfile;
import com.mobilyzer.util.Logger;
import com.mobilyzer.util.PhoneUtils;

import android.content.Context;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;

/**
 * A basic power manager implementation that decides whether a measurement can be scheduled based on
 * the current battery level: no measurements will be scheduled if the current battery is lower than
 * a threshold.
 */
public class ResourceCapManager {



  /** The minimum threshold below which no measurements will be scheduled */
  private int minBatteryThreshold;
  private Context context = null;
  private int dataLimit;// in Byte
  private DataUsageProfile dataUsageProfile;

  // Constants for how much data can be consumed under each profile
  private static int UNLIMITED_LIMIT = -1;
  private static int PROFILE1_LIMIT = 50 * 1024 * 1024;
  private static int PROFILE2_LIMIT = 100 * 1024 * 1024;
  private static int PROFILE3_LIMIT = 250 * 1024 * 1024;
  private static int PROFILE4_LIMIT = 500 * 1024 * 1024;

  // Looking up various phone util data uses the network.
  // It's hard to measure how much.
  // The good news is that this value is basically constant!
  public static int PHONEUTILCOST = 3 * 1024;

  public ResourceCapManager(int batteryThresh, Context context) {
    this.minBatteryThreshold = batteryThresh;
    this.dataLimit = PROFILE3_LIMIT;
    this.context = context;
    this.dataUsageProfile = DataUsageProfile.PROFILE3;
  }

  /**
   * Sets the minimum battery percentage below which measurements cannot be run.
   * 
   * @param batteryThresh the battery percentage threshold between 0 and 100
   */
  public synchronized void setBatteryThresh(int batteryThresh) throws IllegalArgumentException {
    // if (batteryThresh < 0 || batteryThresh > 100) {
    // throw new IllegalArgumentException("batteryCap must fall between 0 and 100, inclusive");
    // }
    this.minBatteryThreshold = batteryThresh;
  }

  public synchronized int getBatteryThresh() {
    return this.minBatteryThreshold;
  }

  /**
   * Given a data profile, set the data limit and profile code accordingly.
   * 
   * If an invalid code is given, leave as default (250 MB) and print a warning.
   * 
   * @param dataLimitStr String describing the profile
   */
  public synchronized void setDataUsageLimit(DataUsageProfile profile) {
    // if (dataLimitStr.equals("50 MB")) {
    // dataLimit = PROFILE1_LIMIT;
    // dataUsageProfile = DataUsageProfile.PROFILE1;
    // } else if (dataLimitStr.equals("100 MB")) {
    // dataLimit = PROFILE2_LIMIT;
    // dataUsageProfile = DataUsageProfile.PROFILE2;
    // } else if (dataLimitStr.equals("250 MB")) {
    // dataLimit = PROFILE3_LIMIT;
    // dataUsageProfile = DataUsageProfile.PROFILE3;
    // } else if (dataLimitStr.equals("500 MB")) {
    // dataLimit = PROFILE4_LIMIT;
    // dataUsageProfile = DataUsageProfile.PROFILE4;
    // } else if (dataLimitStr.equals("Unlimited")) {
    // dataLimit = UNLIMITED_LIMIT;
    // dataUsageProfile = DataUsageProfile.UNLIMITED;
    // } else {
    // Logger.w("Specified limit " + dataLimitStr + " not found!");
    // }
    dataUsageProfile=profile;
    if (profile.equals(DataUsageProfile.PROFILE1)) {
      dataLimit = PROFILE1_LIMIT;
    } else if (profile.equals(DataUsageProfile.PROFILE2)) {
      dataLimit = PROFILE2_LIMIT;
    } else if (profile.equals(DataUsageProfile.PROFILE3)) {
      dataLimit = PROFILE3_LIMIT;
    } else if (profile.equals(DataUsageProfile.PROFILE4)) {
      dataLimit = PROFILE4_LIMIT;
    } else if (profile.equals(DataUsageProfile.UNLIMITED)) {
      dataLimit = UNLIMITED_LIMIT;
    } else {
      Logger.w("Specified limit profile not found!");
    }

  }

  /**
   * @return The current data limit in bytes.
   */
  public synchronized int getDataLimit() {
    return this.dataLimit;
  }

  /**
   * @return An enum representing the data usage limit.
   */
  public synchronized DataUsageProfile getDataUsageProfile() {
    return this.dataUsageProfile;
  }

  /**
   * Reset the data used in the data usage file to 0. This should never be done unless the file does
   * not exist.
   */
  private void resetDataUsage() {
    File file = new File(context.getFilesDir(), "datausage");
    if (file.exists()) {
      Logger.e("Attempting to overwrite a file that exists!!!!");
    }
    long usageStartTimeSec = (System.currentTimeMillis() / 1000);
    writeDataUsageToFile(0, usageStartTimeSec);
  }


  /**
   * Store the data used this period and the beginning of the period in a file, in the format [time
   * reset, in seconds]_[bytes used].
   * 
   * Note that the data used can be negative, due to a underused data budget from last period.
   * 
   * @param dataUsed The updated amount of data to write
   * @param time The updated time to write
   */
  private synchronized void writeDataUsageToFile(long dataUsed, long time) {
    try {
      FileOutputStream outputStream = context.openFileOutput("datausage", Context.MODE_PRIVATE);
      String usageStat = time + "_" + dataUsed;
      outputStream.write(usageStat.getBytes());
      Logger.i("Updating data usage: " + dataUsed + " Byte used from " + time);
      outputStream.close();
    } catch (IOException e) {
      Logger.e("Error in creating data usage file");
      e.printStackTrace();
    }
  }

  /**
   * Read the usage data (start of usage period and quantity used in bytes) from the usage data
   * file.
   * 
   * @return An array consisting of the start of the usage period, then the data used so far. If the
   *         file does not exist, returns -1 in each argument.
   */
  private synchronized long[] readUsageFromFile() {
    long[] retval = {-1, -1};
    File file = new File(context.getFilesDir(), "datausage");
    if (!file.exists()) {
      return retval;
    }
    try {
      String content = "";
      BufferedReader br = new BufferedReader(new FileReader(file));
      String line;
      while ((line = br.readLine()) != null) {
        content += line;
      }
      String[] toks = content.split("_");
      long usageStartTimeSec = Long.parseLong(toks[0]);
      long dataUsed = Long.parseLong(toks[1]);

      retval[0] = usageStartTimeSec;
      retval[1] = dataUsed;

      br.close();
    } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    return retval;
  }

  /**
   * Updates the data consumption period: using the current time move ahead to the correct data
   * consumption period, and also update the data used so far.
   * 
   * Data assigned to a previous data period is subtracted; this can go below zero, effectively
   * crediting unused data to future tasks.
   * 
   * @param dataUsed Data consumed since the start of the last period
   * @param usageStartTimeSec Time since the start of the last period
   * @return
   */
  private long setNewDataConsumptionPeriod(long dataUsed, long usageStartTimeSec) {
    long time_per_period = Config.DEFAULT_DATA_MONITOR_PERIOD_DAY * 24 * 60 * 60;

    Logger.i("Finished data consumption period that began at time:" + usageStartTimeSec
      + " having " + dataUsed + " consumed");

    // Figure out how many periods have passed
    int periods =
        (int) (((float) ((long) (System.currentTimeMillis() / 1000) - usageStartTimeSec)) / (float) time_per_period);

    // Update usageStarTimeSec to the appropriate period
    usageStartTimeSec += periods * time_per_period;

    // Discount from the data used data that is budgeted to previous periods.
    // Note that this could go less than zero if we are below budget.
    long datalimit_per_period = (getDataLimit() * Config.DEFAULT_DATA_MONITOR_PERIOD_DAY) / 30;
    dataUsed = dataUsed - (((int) (periods)) * datalimit_per_period);
    Logger.i("Net data usage at start of period: " + dataUsed);

    writeDataUsageToFile(dataUsed, usageStartTimeSec);
    return dataUsed;

  }

  /**
   * Helper function: given the beginning of the data usage period currrently under consideration,
   * determine if we're still in that period.
   * 
   * @param usageStartTimeSec The start of the last stored data usage period
   * @return True if we are still in the same data usage period.
   */
  private boolean isInDataLimitPeriod(long usageStartTimeSec) {
    long timeSoFar = (System.currentTimeMillis() / 1000) - usageStartTimeSec;
    Logger.i("Time passed since data period last changed: " + timeSoFar);
    return timeSoFar <= Config.DEFAULT_DATA_MONITOR_PERIOD_DAY * 24 * 60 * 60;
  }

  /**
   * Determines if the data limit has been exceeded.
   * 
   * If there is no data limit, always returns false. If there is no valid data usage file, creates
   * a new one and returns false.
   * 
   * Otherwise, checks if we are over the limit yet or if we can run another task. If a new data
   * period needs to be started, we do that too. *
   * 
   * @param nextTaskType In the case of a TCP throughput task, we only run it if there is enough
   *        data left.
   * @return True if over the data limit
   * @throws IOException
   */
  public boolean isOverDataLimit(String nextTaskType) throws IOException {
    Logger.i("Checking data limit...");

    if (getDataLimit() == UNLIMITED_LIMIT) {
      Logger.i("No data limit!");
      return false;
    }
    long[] usagedata = readUsageFromFile();
    long usageStartTimeSec = usagedata[0];
    long dataUsed = usagedata[1];

    if (usageStartTimeSec != -1) {
      if (!isInDataLimitPeriod(usageStartTimeSec)) {
        // Update our file to the next period, and update our data usage
        // budget accordingly.
        dataUsed = setNewDataConsumptionPeriod(dataUsed, usageStartTimeSec);
      }
      long dataLimit = (getDataLimit() * Config.DEFAULT_DATA_MONITOR_PERIOD_DAY) / 30;
      Logger.i("Data limit is: " + dataLimit + " Data used is:" + dataUsed);
      if (dataUsed >= dataLimit) {
        Logger.i("Exceeded data limit:  Total data limit:" + getDataLimit());
        return true;
      } else {
        return false;
      }
    }
    // If the file wasn't there we need to reset the data limit period.
    resetDataUsage();
    return false;
  }

  /**
   * Determine how much data was consumed by a task and update the data usage accordingly.
   * 
   * @param result Structure holding the measurement result from which we can extract data usage.
   * @param taskType The type of measurement task completed
   * @throws IOException
   */
  public void updateDataUsage(long taskDataUsed) throws IOException {

    Logger.i("Amount of data used in the last task: " + taskDataUsed);

    long[] usagedata = readUsageFromFile();
    long usageStartTimeSec = usagedata[0];
    long dataUsed = usagedata[1];
    // If we have a valid file
    if (usageStartTimeSec != -1) {
      dataUsed += taskDataUsed;
      if (!isInDataLimitPeriod(usageStartTimeSec)) {
        // If we are in a new data consumption period, update it
        setNewDataConsumptionPeriod(dataUsed, usageStartTimeSec);
      } else {
        // Otherwise just write to a file
        writeDataUsageToFile(dataUsed, usageStartTimeSec);
      }
    } else {
      // If we don't have a data usage file, initialize it with the data just used
      Logger.i("Data usage file not found, creating a new one...");
      usageStartTimeSec = (System.currentTimeMillis() / 1000);
      dataUsed = taskDataUsed;
      writeDataUsageToFile(dataUsed, usageStartTimeSec);
    }
  }



  /**
   * Returns whether a measurement can be run.
   */
  public synchronized boolean canScheduleExperiment() {
    return (PhoneUtils.getPhoneUtils().isCharging() || PhoneUtils.getPhoneUtils()
        .getCurrentBatteryLevel() > minBatteryThreshold);
  }

}
