/*
 * Title:        CloudSim Toolkit
 * Description:  CloudSim (Cloud Simulation) Toolkit for Modeling and Simulation of Clouds
 * Licence:      GPL - http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2009-2012, The University of Melbourne, Australia
 */

package org.cloudbus.cloudsim.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.cloudbus.cloudsim.Cloudlet;
import org.cloudbus.cloudsim.UtilizationModel;
import org.cloudbus.cloudsim.UtilizationModelFull;

/**
 * This class is responsible for reading resource traces from a file and creating a list of jobs.
 * <p>
 * <b>NOTE:</b>
 * <ul>
 * <li>This class can only take <tt>one</tt> trace file of the following format: <i>ASCII text, zip,
 * gz.</i>
 * <li>If you need to load multiple trace files, then you need to create multiple instances of this
 * class <tt>each with a unique 
 *      	 entity name</tt>.
 * <li>If size of the trace file is huge or contains lots of traces please increase the JVM heap
 * size accordingly by using <tt>java -Xmx</tt> option when running the simulation.
 * <li>The default job file size for sending to and receiving from a resource is
 * {@link gridsim.net.Link#DEFAULT_MTU}. However, you can specify the file size by using
 * {@link #setGridletFileSize(int)}.
 * <li>A job run time is only for 1 PE <tt>not</tt> the total number of allocated PEs. Therefore, a
 * Gridlet length is also calculated for 1 PE.<br>
 * For example, job #1 in the trace has a run time of 100 seconds for 2 processors. This means each
 * processor runs job #1 for 100 seconds, if the processors have the same specification.
 * </ul>
 * <p>
 * By default, this class follows the standard workload format as specified in <a
 * href="http://www.cs.huji.ac.il/labs/parallel/workload/">
 * http://www.cs.huji.ac.il/labs/parallel/workload/</a> <br>
 * However, you can use other format by calling the below methods before running the simulation:
 * <ul>
 * <li> {@link #setComment(String)}
 * <li> {@link #setField(int, int, int, int, int)}
 * </ul>
 * 
 * @author Anthony Sulistio and Marcos Dias de Assuncao
 * @since 5.0
 * 
 * @see Workload
 */
public class WorkloadFileReader implements WorkloadModel {

	private final File file; // file name

	private final int rating; // a PE rating

	private ArrayList<Cloudlet> jobs = null; // a list for getting all the

	// Gridlets

	// using Standard Workload Format
	private int JOB_NUM = 1 - 1; // job number

	private int SUBMIT_TIME = 2 - 1; // submit time of a Gridlet

	private final int RUN_TIME = 4 - 1; // running time of a Gridlet

	private final int NUM_PROC = 5 - 1; // number of processors needed for a

	// Gridlet
	private int REQ_NUM_PROC = 8 - 1; // required number of processors

	private int REQ_RUN_TIME = 9 - 1; // required running time

	private final int USER_ID = 12 - 1; // if of user who submitted the job

	private final int GROUP_ID = 13 - 1; // if of group of the user who

	// submitted the
	// job
	private int MAX_FIELD = 18; // max number of field in the trace file

	private String COMMENT = ";"; // a string that denotes the start of a

	// comment
	private static final int IRRELEVANT = -1; // irrelevant number

	private String[] fieldArray = null; // a temp array storing all the fields

	/**
	 * Create a new {@link WorkloadFileReader} object.
	 * 
	 * @param fileName the workload trace filename in one of the following format: <i>ASCII text,
	 *        zip, gz.</i>
	 * @param rating the resource's PE rating
	 * @throws FileNotFoundException
	 * @throws IllegalArgumentException This happens for the following conditions:
	 *         <ul>
	 *         <li>the workload trace file name is null or empty
	 *         <li>the resource PE rating <= 0
	 *         </ul>
	 * @pre fileName != null
	 * @pre rating > 0
	 * @post $none
	 */
	public WorkloadFileReader(final String fileName, final int rating) throws FileNotFoundException {
		if (fileName == null || fileName.length() == 0) {
			throw new IllegalArgumentException("Invalid trace file name.");
		} else if (rating <= 0) {
			throw new IllegalArgumentException("Resource PE rating must be > 0.");
		}

		file = new File(fileName);
		if (!file.exists()) {
			throw new FileNotFoundException("Workload trace " + fileName + " does not exist");
		}

		this.rating = rating;
	}

	/**
	 * Reads job information from a given file.
	 * 
	 * @return the list of gridlets read from the file; <code>null</code> in case of failure.
	 */
	@Override
	public ArrayList<Cloudlet> generateWorkload() {
		if (jobs == null) {
			jobs = new ArrayList<Cloudlet>();

			// create a temp array
			fieldArray = new String[MAX_FIELD];

			try {
				if (file.getName().endsWith(".gz")) {
					readGZIPFile(file);
				} else if (file.getName().endsWith(".zip")) {
					readZipFile(file);
				} else {
					readFile(file);
				}
			} catch (final FileNotFoundException e) {
			} catch (final IOException e) {
			}
		}

		return jobs;
	}

	/**
	 * Identifies the start of a comment line.
	 * 
	 * @param cmt a character that denotes the start of a comment, e.g. ";" or "#"
	 * @return <code>true</code> if it is successful, <code>false</code> otherwise
	 * @pre comment != null
	 * @post $none
	 */
	public boolean setComment(final String cmt) {
		boolean success = false;
		if (cmt != null && cmt.length() > 0) {
			COMMENT = cmt;
			success = true;
		}
		return success;
	}

	/**
	 * Tells this class what to look in the trace file. This method should be called before the
	 * start of the simulation.
	 * <p>
	 * By default, this class follows the standard workload format as specified in <a
	 * href="http://www.cs.huji.ac.il/labs/parallel/workload/">
	 * http://www.cs.huji.ac.il/labs/parallel/workload/</a> <br>
	 * However, you can use other format by calling this method.
	 * <p>
	 * The parameters must be a positive integer number starting from 1. A special case is where
	 * <tt>jobNum == -1</tt>, meaning the job or gridlet ID starts at 1.
	 * 
	 * @param maxField max. number of field/column in one row
	 * @param jobNum field/column number for locating the job ID
	 * @param submitTime field/column number for locating the job submit time
	 * @param runTime field/column number for locating the job run time
	 * @param numProc field/column number for locating the number of PEs required to run a job
	 * @return <code>true</code> if successful, <code>false</code> otherwise
	 * @throws IllegalArgumentException if any of the arguments are not within the acceptable ranges
	 * @pre maxField > 0
	 * @pre submitTime > 0
	 * @pre runTime > 0
	 * @pre numProc > 0
	 * @post $none
	 */
	public boolean setField(
			final int maxField,
			final int jobNum,
			final int submitTime,
			final int runTime,
			final int numProc) {
		// need to subtract by 1 since array starts at 0.
		if (jobNum > 0) {
			JOB_NUM = jobNum - 1;
		} else if (jobNum == 0) {
			throw new IllegalArgumentException("Invalid job number field.");
		} else {
			JOB_NUM = -1;
		}

		// get the max. number of field
		if (maxField > 0) {
			MAX_FIELD = maxField;
		} else {
			throw new IllegalArgumentException("Invalid max. number of field.");
		}

		// get the submit time field
		if (submitTime > 0) {
			SUBMIT_TIME = submitTime - 1;
		} else {
			throw new IllegalArgumentException("Invalid submit time field.");
		}

		// get the run time field
		if (runTime > 0) {
			REQ_RUN_TIME = runTime - 1;
		} else {
			throw new IllegalArgumentException("Invalid run time field.");
		}

		// get the number of processors field
		if (numProc > 0) {
			REQ_NUM_PROC = numProc - 1;
		} else {
			throw new IllegalArgumentException("Invalid number of processors field.");
		}

		return true;
	}

	// ------------------- PRIVATE METHODS -------------------

	/**
	 * Creates a Gridlet with the given information and adds to the list
	 * 
	 * @param id a Gridlet ID
	 * @param submitTime Gridlet's submit time
	 * @param runTime Gridlet's run time
	 * @param numProc number of processors
	 * @param reqRunTime user estimated run time
	 * @param userID user id
	 * @param groupID user's group id
	 * @pre id >= 0
	 * @pre submitTime >= 0
	 * @pre runTime >= 0
	 * @pre numProc > 0
	 * @post $none
	 */
	private void createJob(
			final int id,
			final long submitTime,
			final int runTime,
			final int numProc,
			final int reqRunTime,
			final int userID,
			final int groupID) {
		// create the cloudlet
		final int len = runTime * rating;
		UtilizationModel utilizationModel = new UtilizationModelFull();
		final Cloudlet wgl = new Cloudlet(
				id,
				len,
				numProc,
				0,
				0,
				utilizationModel,
				utilizationModel,
				utilizationModel);
		jobs.add(wgl);
	}

	/**
	 * Extracts relevant information from a given array
	 * 
	 * @param array an array of String
	 * @param line a line number
	 * @pre array != null
	 * @pre line > 0
	 */
	private void extractField(final String[] array, final int line) {
		try {
			Integer obj = null;

			// get the job number
			int id = 0;
			if (JOB_NUM == IRRELEVANT) {
				id = jobs.size() + 1;
			} else {
				obj = new Integer(array[JOB_NUM].trim());
				id = obj.intValue();
			}

			// get the submit time
			final Long l = new Long(array[SUBMIT_TIME].trim());
			final long submitTime = l.intValue();

			// get the user estimated run time
			obj = new Integer(array[REQ_RUN_TIME].trim());
			final int reqRunTime = obj.intValue();

			// if the required run time field is ignored, then use
			// the actual run time
			obj = new Integer(array[RUN_TIME].trim());
			int runTime = obj.intValue();

			final int userID = new Integer(array[USER_ID].trim()).intValue();
			final int groupID = new Integer(array[GROUP_ID].trim()).intValue();

			// according to the SWF manual, runtime of 0 is possible due
			// to rounding down. E.g. runtime is 0.4 seconds -> runtime = 0
			if (runTime <= 0) {
				runTime = 1; // change to 1 second
			}

			// get the number of allocated processors
			obj = new Integer(array[REQ_NUM_PROC].trim());
			int numProc = obj.intValue();

			// if the required num of allocated processors field is ignored
			// or zero, then use the actual field
			if (numProc == IRRELEVANT || numProc == 0) {
				obj = new Integer(array[NUM_PROC].trim());
				numProc = obj.intValue();
			}

			// finally, check if the num of PEs required is valid or not
			if (numProc <= 0) {
				numProc = 1;
			}
			createJob(id, submitTime, runTime, numProc, reqRunTime, userID, groupID);
		} catch (final Exception e) {

		}
	}

	/**
	 * Breaks a line of string into many fields.
	 * 
	 * @param line a line of string
	 * @param lineNum a line number
	 * @pre line != null
	 * @pre lineNum > 0
	 * @post $none
	 */
	private void parseValue(final String line, final int lineNum) {
		// skip a comment line
		if (line.startsWith(COMMENT)) {
			return;
		}

		final String[] sp = line.split("\\s+"); // split the fields based on a
		// space
		int len = 0; // length of a string
		int index = 0; // the index of an array

		// check for each field in the array
		for (final String elem : sp) {
			len = elem.length(); // get the length of a string

			// if it is empty then ignore
			if (len == 0) {
				continue;
			}
			fieldArray[index] = elem;
			index++;
		}

		if (index == MAX_FIELD) {
			extractField(fieldArray, lineNum);
		}
	}

	/**
	 * Reads a text file one line at the time
	 * 
	 * @param fl a file name
	 * @return <code>true</code> if successful, <code>false</code> otherwise.
	 * @throws IOException if the there was any error reading the file
	 * @throws FileNotFoundException if the file was not found
	 */
	private boolean readFile(final File fl) throws IOException, FileNotFoundException {
		boolean success = false;
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new InputStreamReader(new FileInputStream(fl)));

			// read one line at the time
			int line = 1;
			while (reader.ready()) {
				parseValue(reader.readLine(), line);
				line++;
			}

			reader.close();
			success = true;
		} finally {
			if (reader != null) {
				reader.close();
			}
		}

		return success;
	}

	/**
	 * Reads a gzip file one line at the time
	 * 
	 * @param fl a gzip file name
	 * @return <code>true</code> if successful; <code>false</code> otherwise.
	 * @throws IOException if the there was any error reading the file
	 * @throws FileNotFoundException if the file was not found
	 */
	private boolean readGZIPFile(final File fl) throws IOException, FileNotFoundException {
		boolean success = false;
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(fl))));

			// read one line at the time
			int line = 1;
			while (reader.ready()) {
				parseValue(reader.readLine(), line);
				line++;
			}

			reader.close();
			success = true;
		} finally {
			if (reader != null) {
				reader.close();
			}
		}

		return success;
	}

	/**
	 * Reads a Zip file.
	 * 
	 * @param fl a zip file name
	 * @return <code>true</code> if reading a file is successful; <code>false</code> otherwise.
	 * @throws IOException if the there was any error reading the file
	 */
	private boolean readZipFile(final File fl) throws IOException {
		boolean success = false;
		ZipFile zipFile = null;
		try {
			BufferedReader reader = null;

			// ZipFile offers an Enumeration of all the files in the file
			zipFile = new ZipFile(fl);
			final Enumeration<? extends ZipEntry> e = zipFile.entries();
			while (e.hasMoreElements()) {
				success = false; // reset the value again
				final ZipEntry zipEntry = e.nextElement();

				reader = new BufferedReader(new InputStreamReader(zipFile.getInputStream(zipEntry)));

				// read one line at the time
				int line = 1;
				while (reader.ready()) {
					parseValue(reader.readLine(), line);
					line++;
				}

				reader.close();
				success = true;
			}
		} finally {
			if (zipFile != null) {
				zipFile.close();
			}
		}

		return success;
	}
}
