package org.bench4q.agent.scenario.engine;

import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Observable;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.log4j.Logger;
import org.bench4q.share.exception.Bench4QRunTimeException;
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel;
import org.bench4q.share.models.agent.scriptrecord.ScheduleModel.PointModel;

/*
 * Segments in Schedule is sorted asc by Segment.start.time
 */
public class Schedule extends Observable {
	private static final int SCHEDULE_CYCLE = 3000;
	private final List<Segment> segments;
	private final long beginTime;
	private final Timer timer;
	private final int maxLoad;
	private volatile boolean reachEnd;
	private final Logger logger = Logger.getLogger(Schedule.class);

	public List<Segment> getSegments() {
		return segments;
	}

	public int getMaxLoad() {
		return maxLoad;
	}

	public boolean hasReachEnd() {
		return this.reachEnd;
	}

	private void reachEnd() {
		this.reachEnd = true;
	}

	public Schedule(List<Segment> segments, int maxLoad) {
		if (segments == null || segments.size() == 0) {
			throw new Bench4QRunTimeException(
					"Can't init a schedul with zero segment");
		}
		this.segments = segments;
		this.beginTime = System.currentTimeMillis();
		this.reachEnd = false;
		this.maxLoad = maxLoad;
		this.timer = new Timer();
	}

	public long getScheduleRange() {
		return this.getSegments().get(this.getSegments().size() - 1).end
				.getTime() - this.getSegments().get(0).start.getTime();
	}

	public void begin() {
		timer.scheduleAtFixedRate(new TimerTask() {
			@Override
			public void run() {
				long time = System.currentTimeMillis();
				Segment segment = getSegment(time - beginTime);
				if (segment == null) {
					// exceed the range of execute, should let the context stop
					// the test
					logger.info("Execution Over!");
					notifyObservers(0);
					return;
				}
				int load = segment.loadFor(time - beginTime);
				logger.info("Time: " + time + " ; Load: " + load);
				Schedule.this.setChanged();
				notifyObservers(load);
			}
		}, 0, SCHEDULE_CYCLE);
	}

	void stop() {
		this.timer.cancel();
	}

	/**
	 * 
	 * @param time
	 *            , is the relative time from begin
	 * @return get the segment by binary search
	 */
	public Segment getSegment(long time) {
		if (this.getSegments() == null || this.getSegments().size() < 1
				|| time < this.getSegments().get(0).start.getTime()) {
			throw new Bench4QRunTimeException(
					"can't getSegment when segments' size is LT 2");
		}
		if (time >= this.getSegments().get(this.getSegments().size() - 1).end
				.getTime()) {
			this.reachEnd();
			return null;
		}
		int begin = 0, end = this.getSegments().size(), mid = (begin + end) / 2;
		while (begin <= end) {
			Segment midSegment = this.getSegments().get(mid);
			if (midSegment.end.getTime() < time) {
				begin = mid + 1;
			} else if (midSegment.start.getTime() > time) {
				end = mid - 1;
			} else {
				return midSegment;
			}
			mid = (begin + end) / 2;
		}
		throw new Bench4QRunTimeException("Should not come to this place");
	}

	/**
	 * 
	 * @param relativeTime
	 *            , relativeTime from beginDate
	 * @return
	 */
	public int loadFor(long relativeTime) {
		Segment segment = getSegment(relativeTime);
		if (segment == null) {
			throw new IllegalArgumentException();
		}
		return segment.loadFor(relativeTime);
	}

	public static class Segment {
		private final Point start;
		private final Point end;
		// growthUnit is the amount that'll increase per second
		private final float growthUnit;

		public Segment(Point startPoint, Point endPoint) {
			this.start = startPoint.copy();
			this.end = endPoint.copy();
			long timeDifference = this.end.getTime() / 1000
					- this.start.getTime() / 1000;
			if (timeDifference < 0 || this.start.getTime() < 0l
					|| this.end.getTime() < 0l) {
				throw new Bench4QRunTimeException(
						"The end time in TestScehdul cannot be less than start time");
			}
			this.growthUnit = (float) (timeDifference == 0 ? 0
					: (double) (this.end.getLoad() - this.start.getLoad())
							/ (double) (timeDifference));
		}

		public int loadFor(long timeFromBegin) {
			if (timeFromBegin < this.start.getTime()
					|| timeFromBegin > this.end.getTime()) {
				throw new IllegalArgumentException();
			}
			long diffFromStart = timeFromBegin - this.start.getTime();
			return (int) (this.start.getLoad() + (diffFromStart * growthUnit / 1000));
		}
	}

	public static class Point {
		// This time is the relative value from begin
		private final long time;
		private final int load;

		public long getTime() {
			return time;
		}

		public int getLoad() {
			return load;
		}

		public Point(long time, int load) {
			if (load < 0) {
				throw new IllegalArgumentException(
						"Load can't be negtive number!");
			}
			this.time = time;
			this.load = load;
		}

		public Point copy() {
			return new Point(this.getTime(), this.getLoad());
		}
	}

	public static Schedule build(ScheduleModel scheduleModel) {
		List<Segment> segements = new LinkedList<Schedule.Segment>();
		int maxLoad = extractSegmentsAndGetMaxLoad(scheduleModel.getPoints(),
				segements);
		Schedule schedule = new Schedule(segements, maxLoad);
		return schedule;
	}

	private static int extractSegmentsAndGetMaxLoad(
			List<PointModel> pointModels, List<Segment> result) {
		int maxLoad = 0;
		List<Point> points = new LinkedList<Schedule.Point>();
		for (PointModel model : pointModels) {
			maxLoad = Math.max(maxLoad, model.getLoad());
			points.add(new Point(model.getTime(), model.getLoad()));
		}
		Collections.sort(points, new Comparator<Point>() {
			@Override
			public int compare(Point o1, Point o2) {
				return (int) (o1.getTime() - o2.getTime());
			}
		});
		for (int i = 0; i < points.size() - 1; i++) {
			result.add(new Segment(points.get(i), points.get(i + 1)));
		}
		return maxLoad;
	}
}
