package freenet.support;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.TreeMap;

import freenet.node.FastRunnable;
import freenet.support.Logger.LogLevel;
import freenet.support.io.NativeThread;

public class PrioritizedTicker implements Ticker, Runnable {
	
	private static volatile boolean logMINOR;

	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	private final static class Job {
		final String name;
		final Runnable job;
		Job(String name, Runnable job) {
			this.name = name;
			this.job = job;
		}

		@Override
		public boolean equals(Object o) {
			if(!(o instanceof Job)) return false;
			// Ignore the name, we are only interested in the job, needed for noDupes.
			return ((Job)o).job == job;
		}
		
		@Override
		public int hashCode() {
			return job.hashCode();
		}
	}
	
	/** ~= Ticker :) */
	private final TreeMap<Long, Object> timedJobsByTime;
	private final HashMap<Job, Long> timedJobsQueued;
	final NativeThread myThread;
	final Executor executor;
	static final int MAX_SLEEP_TIME = 200;
	
	public PrioritizedTicker(Executor executor, int portNumber) {
		this.executor = executor;
		timedJobsByTime = new TreeMap<Long, Object>();
		timedJobsQueued = new HashMap<Job, Long>();
		myThread = new NativeThread(this, "Ticker thread for " + portNumber, NativeThread.MAX_PRIORITY, false);
		myThread.setDaemon(true);
	}
	
	public void start() {
		Logger.normal(this, "Starting Ticker");
		System.out.println("Starting Ticker");
		myThread.start();
	}

	@Override
	public void run() {
		if(logMINOR) Logger.minor(this, "In Ticker.run()");
		while(true) {
			try {
				realRun();
			} catch(Throwable t) {
				Logger.error(this, "Caught in PacketSender: " + t, t);
				System.err.println("Caught in PacketSender: " + t);
				t.printStackTrace();
			}
		}
	}
	
	private void realRun() {
		long now = System.currentTimeMillis();
		
		List<Job> jobsToRun = null;
		
		long sleepTime = MAX_SLEEP_TIME;

		synchronized(timedJobsByTime) {
			while(!timedJobsByTime.isEmpty()) {
				Long tRun = timedJobsByTime.firstKey();
				if(tRun <= now) {
					if(jobsToRun == null)
						jobsToRun = new ArrayList<Job>();
					Object o = timedJobsByTime.remove(tRun);
					if(o instanceof Job[]) {
						for(Job r: (Job[]) o) {
							jobsToRun.add(r);
							timedJobsQueued.remove(r);
						}
					} else {
						Job r = (Job) o;
						jobsToRun.add(r);
						timedJobsQueued.remove(r);
					}
				} else {
					sleepTime = Math.min(sleepTime, tRun - now);
					break;
				}
			}
		}

		if(jobsToRun != null)
			for(Job r : jobsToRun) {
				if(logMINOR)
					Logger.minor(this, "Running " + r);
				if(r.job instanceof FastRunnable)
					// Run in-line

					try {
						r.job.run();
					} catch(Throwable t) {
						Logger.error(this, "Caught " + t + " running " + r, t);
					}
				else
					try {
						executor.execute(r.job, r.name, true);
					} catch(Throwable t) {
						Logger.error(this, "Caught in PacketSender: " + t, t);
						System.err.println("Caught in PacketSender: " + t);
						t.printStackTrace();
                                                System.err.println("Will retry above failed operation...");
                                                queueTimedJob(r.job, r.name, 200, true, false);
					}
			}

		if(sleepTime > 0) {
			try {
			    sleep(sleepTime);
			} catch(InterruptedException e) {
				// Ignore, just wake up. Probably we got interrupt()ed
				// because a new job came in.
			}
		}
	}

	protected void sleep(long sleepTime) throws InterruptedException {
        if(logMINOR)
            Logger.minor(this, "Sleeping for " + sleepTime);
        synchronized(this) {
            wait(sleepTime);
        }
    }

    @Override
	public void queueTimedJob(Runnable job, long offset) {
		queueTimedJob(job, "Scheduled job: "+job, offset, false, false);
	}
	
	/**
	 * Queue a job at a specific time (offset in milliseconds from "now").
	 * @param runner The job to run. FastRunnable's get run directly on the PacketSender thread.
	 * @param name The name of the job, the thread running it will temporarily take this name,
	 * assuming it is run on a separate thread.
	 * @param offset The time at which to run the job in milliseconds after
	 * System.currentTimeMillis().
	 * @param runOnTickerAnyway If false, run jobs with offset <=0 on the ticker, to preserve
	 * their thread priorities; if true, jobs to run immediately through the executor (which
	 * normally will also preserve thread priorities, but may need to call back via
	 * runOnTickerAnyway=true if it needs to increase the thread priority).
	 * @param noDupes Don't run this job if it is already scheduled. Relatively expensive, O(n)
	 * with queued jobs. Necessary for Announcer to ensure that we don't get exponentially
	 * increasing numbers of announcement check jobs queued, while ensuring that we do always
	 * have one queued within the given period.
	 */
	@Override
	public void queueTimedJob(Runnable runner, String name, long offset, boolean runOnTickerAnyway, boolean noDupes) {
		// Run directly *if* that won't cause any priority problems.
		long now = System.currentTimeMillis();
        if(offset < 0) offset = 0;
		queueTimedJobInner(runner, name, now+offset, offset, runOnTickerAnyway, noDupes);
	}

	/** Queue a job at a specific time (absolute time in milliseconds). If the time given has
	 * passed already, then run the job ASAP. 
	 * @param time The time at which to run the job. @see System.currentTimeMillis()
	 */
	@Override
	public void queueTimedJobAbsolute(Runnable runner, String name, long time, 
            boolean runOnTickerAnyway, boolean noDupes) {
	    long now = System.currentTimeMillis();
	    queueTimedJobInner(runner, name, time, time-now, runOnTickerAnyway, noDupes);
	}
	
	/** Queue a job at a specific absolute time. 
	 * @param runJobAt The absolute time at which the job should run.
	 * @param offset The offset in milliseconds from "now" (i.e. some recent call to 
	 * System.currentTimeMillis()). */
    private void queueTimedJobInner(Runnable runner, String name, long runJobAt, long offset, 
            boolean runOnTickerAnyway, boolean noDupes) {
        if(noDupes) runOnTickerAnyway = true;
        if(offset <= 0 && !runOnTickerAnyway) {
            if(logMINOR) Logger.minor(this, "Running directly: "+runner);
            executor.execute(runner, name);
            return;
        }
        Job job = new Job(name, runner);
        synchronized(timedJobsByTime) {
            if(noDupes) {
                Long alreadyQueuedAt = timedJobsQueued.get(job);
                if(alreadyQueuedAt != null) {
                    if(alreadyQueuedAt <= runJobAt) {
                        Logger.normal(this, "Not re-running as already queued: "+runner+" for "+name);
                        return;
                    } else {
                        // Delete the existing job because the new job will run first.
                        removeQueuedJobInner(job, alreadyQueuedAt);
                    }
                }
            }
            Object o = timedJobsByTime.get(runJobAt);
            if(o == null)
                timedJobsByTime.put(runJobAt, job);
            else if(o instanceof Job)
                timedJobsByTime.put(runJobAt, new Job[]{(Job) o, job});
            else if(o instanceof Job[]) {
                Job[] r = (Job[]) o;
                Job[] jobs = Arrays.copyOf(r, r.length+1);
                jobs[jobs.length - 1] = job;
                timedJobsByTime.put(runJobAt, jobs);
            }
            timedJobsQueued.put(job, runJobAt);
        }
        if(offset < MAX_SLEEP_TIME) {
            wakeUp();
        }
    }
	
	/** Wake up, and run any queued jobs. */
	void wakeUp() {
		// Wake up if needed
		synchronized(this) {
			notifyAll();
		}
	}

	@Override
	public Executor getExecutor() {
		return executor;
	}

	int queuedJobs() {
		synchronized(timedJobsByTime) {
			return timedJobsQueued.size();
		}
	}

    int queuedJobsUniqueTimes() {
        synchronized(timedJobsByTime) {
            return timedJobsByTime.size();
        }
    }

	@Override
	/* Remove a queued job.
	 * @param runnable The job to remove. If this is currently queued, it will be 
	 * removed. The Ticker should not throw if the job is not queued. */
	public void removeQueuedJob(Runnable runnable) {
		Job job = new Job(null, runnable);
		synchronized(timedJobsByTime) {
			Long t = timedJobsQueued.remove(job);
			if(t != null) {
			    removeQueuedJobInner(job, t);
			}
		}
	}

	/** Remove a queued job from the internal structures other than timedJobsQueued. The
	 * caller must check that it is present in timedJobsQueued, remove from that structure, and 
	 * call this method, all inside the timedJobsByTime lock. 
	 * @param job The job to remove.
	 * @param t The time at which is it scheduled.
	 */
	private void removeQueuedJobInner(Job job, Long t) {
        Object o = timedJobsByTime.get(t);
        assert(o != null);
        if(o instanceof Job) {
            assert(o.equals(job));
            timedJobsByTime.remove(t);
        } else {
            Job[] jobs = (Job[]) o;
            if(jobs.length == 1) {
                assert(jobs[0].equals(job));
                timedJobsByTime.remove(t);
            } else {
                Job[] newJobs = new Job[jobs.length-1];
                int x = 0;
                for(Job oldjob : jobs) {
                    if(oldjob.equals(job)) {
                        continue;
                    }
                    newJobs[x++] = oldjob;
                    assert(x != jobs.length); // Must be in jobs array.
                }
                assert(x != 0); // Not duplicated.
                if (x == 1) {
                    timedJobsByTime.put(t, newJobs[0]);
                } else {
                    if(x != newJobs.length)
                        newJobs = Arrays.copyOf(newJobs, x);
                    timedJobsByTime.put(t, newJobs);
                    assert(x == jobs.length-1);
                }
            }
        }
    }
}
