/*******************************************************************************
* 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
*
*      https://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 net.adoptopenjdk.casa.workload_sessions;

import net.adoptopenjdk.casa.util.Utilities;
import net.adoptopenjdk.casa.util.format.Alignment;
import net.adoptopenjdk.casa.util.format.CellException;
import net.adoptopenjdk.casa.util.format.RowException;
import net.adoptopenjdk.casa.util.format.Table;
import net.adoptopenjdk.casa.workload_sessions.configuration.PayloadConfiguration;
import net.adoptopenjdk.casa.workload_sessions.configuration.PayloadSetConfiguration;

/**
 * Encapsulates the builders associated with a particular workload.  
 * 
 *  
 *
 */
public class PayloadBuilderSet {
	// The set's configuration
	private final PayloadSetConfiguration payloadSetConfiguration;

	// The total paid to the set so far.
	private double totalPayment;

	// All builders in this set. Does no include subsets.
	private PayloadBuilder[] builders;

	// The workload this set is a part of.
	private Workload workload;

	// Each set has its own container.
	private PayloadContainer container;

	/**
	 * Initializes the set from the given configuration for the given workload,
	 * including initializing the container.
	 * 
	 * @param payloadSetConfiguration
	 * @param workload
	 */
	protected PayloadBuilderSet(PayloadSetConfiguration payloadSetConfiguration, Workload workload) {
		super();

		Event.ASSERTION.issue(payloadSetConfiguration == null, "payloadSetConfiguration is null");

		this.workload = workload;
		this.payloadSetConfiguration = payloadSetConfiguration;
		this.totalPayment = 0;

		// Create the container.
		this.container = new PayloadContainer(workload, this);

		PayloadConfiguration[] payloadConfigurations = payloadSetConfiguration.getPayloadConfigurations();

		// Create the builders from the configurations.
		this.builders = new PayloadBuilder[payloadConfigurations.length];

		for (int i = 0; i < payloadConfigurations.length; i++) {
			builders[i] = new PayloadBuilder(payloadConfigurations[i], this, workload);
		}

	}

	/**
	 * Returns the largest value of getExpectedPeakUsageTime() returned by a builder
	 * in this set.
	 * 
	 * @return an estimate (in seconds) of the time until the live set generated by
	 *         this builder set reaches its peak.
	 */
	protected double getExpectedPeakUsageTime() {
		double time = 0;
		for (PayloadBuilder builder : builders) {
			if (time < builder.getExpectedPeakUsageTime()) {
				time = builder.getExpectedPeakUsageTime();
			}
		}

		return time;
	}

	/**
	 * Sums the values returned by getExpectedPeakUsage() from every builder in this
	 * set.
	 * 
	 * @return - an estimate (in bytes) of the size of the live-set resulting from
	 *         this set after the expected peak usage time has elapsed.
	 */
	protected double getExpectedPeakContainerUsage() {
		double peakUsage = 0;
		for (PayloadBuilder builder : builders) {
			peakUsage += builder.getExpectedPeakUsage();
		}

		return peakUsage;
	}

	/**
	 * Returns a textual representation of the encapsulated builders.
	 */
	public String toString() {
		StringBuilder stringBuilder = new StringBuilder();

		appendToStringBuilder(stringBuilder);

		return stringBuilder.toString();
	}

	/**
	 * Appends a string representation of the set to the given StringBuilder
	 * 
	 * @param stringBuilder
	 */
	public void appendToStringBuilder(StringBuilder stringBuilder) {
		// Create a table with information about the set
		try {
			Alignment[] alignment = new Alignment[] { Alignment.LEFT, Alignment.RIGHT };
			Table table = new Table();
			table.addHeadings(table.new Headings(new String[] { "PAYLOAD SET ", payloadSetConfiguration.getIDString() },
					new int[] { 24, 20 }, alignment, false, false));
			table.addRow(
					table.new Row(
							new String[] { "Pay rate",
									Utilities.formatDataSize(payloadSetConfiguration.getDataRate()) + "/s"
											+ String.format("(%.0ftr/s)",
													payloadSetConfiguration.getDataRate()
															* payloadSetConfiguration.getTransactionsPerByte()) },
							alignment));
			table.addRow(table.new Row(
					new String[] { "Expected peak usage", Utilities.formatDataSize(getExpectedPeakContainerUsage())
							+ " at " + Utilities.formatTime(getExpectedPeakUsageTime()) },
					alignment));
			table.addRow(table.new Row(
					new String[] { "Payment interval",
							Utilities
									.formatTime(payloadSetConfiguration
											.getExpectedStartTime(workload.getWorkloadConfiguration().getDuration()))
									+ "-"
									+ Utilities.formatTime(payloadSetConfiguration
											.getExpectedEndTime(workload.getWorkloadConfiguration().getDuration()))
									+ "("
									+ Utilities.formatTime(payloadSetConfiguration
											.getExpectedLifespan(workload.getWorkloadConfiguration().getDuration()))
									+ ")" },
					alignment));
			table.addRow(table.new Row(
					new String[] { "Container Lists", payloadSetConfiguration.getNumPayloadContainerLists() + "" },
					alignment));

			table.appendToStringBuilder(stringBuilder);
			stringBuilder.append("\n");
		} catch (CellException | RowException e) {
			Event.WARNING.issue(e);
			stringBuilder.append("[Invalid Table]\n");
		}

		// Create a table with information about the individual Payloads
		try {
			Table table = new Table();
			table.addHeadings(table.new Headings(
					new String[] { "ID", "Type", "%", "Size", "Lifespan", "Period", "Peak", "...at" },
					new int[] { 5, 10, 8, 8, 8, 8, 8, 8 }, Alignment.LEFT));
			for (PayloadBuilder builder : builders) {
				table.addRow(table.new Row(new String[] { String.valueOf(builder.getPayloadConfiguration().getID()),
						builder.getPayloadConfiguration().getPayloadType().toString(),
						Utilities.formatProportionAsPercent(
								builder.getPayloadConfiguration().getProportionOfAllocation(), 3),
						Utilities.formatDataSize(builder.getPayloadConfiguration().getSize()),
						Utilities.formatTime(builder.getPayloadConfiguration().getLifespan()),
						Utilities.formatTime(builder.getPayloadConfiguration().getAllocationPeriod()),
						Utilities.formatDataSize(builder.getExpectedPeakUsage()),
						Utilities.formatTime(builder.getExpectedPeakUsageTime()) }, Alignment.RIGHT));
			}
			table.addLine();

			table.appendToStringBuilder(stringBuilder);
		} catch (CellException | RowException e) {
			Event.WARNING.issue(e);
			stringBuilder.append("[Invalid Table]");
		}
	}

	/**
	 * Gets the encapsulated array of builders.
	 * 
	 * @return
	 */
	protected PayloadBuilder[] getBuilders() {
		return builders;
	}

	/**
	 * Adds the given value to the total payment counter.
	 * 
	 * @param amount
	 */
	protected void adjustTotalPayment(double amount) {
		totalPayment += amount;
	}

	/**
	 * Gets the measured rate of payment of this builder set from the time it
	 * started.
	 * 
	 * @return the payment rate in bytes per second.
	 */
	protected double getPaymentRate() {
		return totalPayment / (workload.getElapsedTime()
				- payloadSetConfiguration.getExpectedStartTime(workload.getWorkloadConfiguration().getDuration()));
	}

	/**
	 * Calculate the amount to allocate based on the time taken during the last
	 * cycle and scale it based on the ratio of the average payment rate so far to
	 * the target data rate.
	 * 
	 * @param lastCycleTime - the time elapsed during the last payment cycle.
	 * @return the amount to pay the builders in this set for this cycle, assuming
	 *         they are active.
	 */
	protected double getCyclePayment(double lastCycleTime) {
		Event.ASSERTION.issue(lastCycleTime < 0, "Negative cycle time");

		// Idealized cycle payment
		double basePayment = lastCycleTime * getConfiguration().getDataRate();

		// Slightly above 1 to correct for timing errors.
		double timingCorrectionFactor = 1.1;

		// Default upward correction
		double pullUpPayment = basePayment * timingCorrectionFactor;

		// Correct more aggressively downward.
		double pullDownPayment = basePayment / (timingCorrectionFactor * timingCorrectionFactor);

		// If the current payment rate is zero, a ratio of 1 is used to bootstrap the
		// algorithm.
		double ratio = (getPaymentRate() > 0) ? getConfiguration().getDataRate() / getPaymentRate() : 1;

		// Pick the max or min base allocation ratio based on whether the current
		// payment rate is too high or too low.
		double cycleAllocation = (ratio > 1.0) // When in doubt, pull down.
				? pullUpPayment
				: pullDownPayment;

		// Scale the allocation based on the ratio.
		cycleAllocation *= ratio;

		// Cap payment at the data rate.
		cycleAllocation = Math.min(getConfiguration().getDataRate() * 2, cycleAllocation);

		Event.ASSERTION.issue(cycleAllocation < 0, "Negative allocation");

		return cycleAllocation;
	}

	/**
	 * The configuration for this set.
	 * 
	 * @return
	 */
	protected PayloadSetConfiguration getConfiguration() {
		return payloadSetConfiguration;
	}

	/**
	 * Determines whether or not this set is currently active and payable based on
	 * its expected start and end times.
	 * 
	 * @return true if this set should be paid. false if this set should not be
	 *         paid.
	 */
	protected boolean isActive() {
		double time = workload.getElapsedTime();

		if (payloadSetConfiguration.getExpectedStartTime(workload.getWorkloadConfiguration().getDuration()) <= time
				&& payloadSetConfiguration
						.getExpectedEndTime(workload.getWorkloadConfiguration().getDuration()) >= time)
			return true;
		else
			return false;
	}

	/**
	 * Gets the set-specific container.
	 * 
	 * @return
	 */
	protected PayloadContainer getContainer() {
		return container;
	}

	/**
	 * Starts the container
	 */
	protected void startup() {
		container.startup();
	}

	/**
	 * Shuts down the container.
	 * 
	 * @throws InterruptedException
	 */
	protected void shutdown() throws InterruptedException {
		container.shutdown();
	}

	/**
	 * Kills the set's container.
	 */
	protected void kill() {
		container.kill();
	}

	/**
	 * Creates a new PayloadBuilderSet and shifts the start and end values by the
	 * amount specified by the scalar value param
	 * 
	 * @param scalarValue amount we need to shift the start and end times
	 * @return the set with the start/end times shifted
	 */
	public PayloadBuilderSet generateSet(int scalarValue) {
		
		PayloadBuilderSet updatedPayloadBuilderSet = null;
		try {
			PayloadSetConfiguration updatedConfig;
			updatedConfig = (PayloadSetConfiguration) this.payloadSetConfiguration.clone();
			updatedConfig.incrementStartTime(scalarValue);
			updatedConfig.incrementEndTime(scalarValue);
			updatedPayloadBuilderSet = new PayloadBuilderSet(updatedConfig, this.workload);

		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}

		return updatedPayloadBuilderSet;
	}
}
