/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.
 */

/**
 * Timer implementation adapted to Java ME from Apache Harmony (Open Source Java SE)
 * Main changes:
 *  - Java ME finalizer used
 *  - signature test related changes (API to Java 1.3 level)
 *      - some methods made private
 *      - some unsupported method calls commented out
 *  - exception strings hardcoded instead of properties
 */

package java.util;

import com.nokia.mj.impl.rt.support.Finalizer;


/**
 * {@code Timer}s are used to schedule jobs for execution in a background process. A
 * single thread is used for the scheduling and this thread has the option of
 * being a daemon thread. By calling {@code cancel} you can terminate a
 * {@code Timer} and its associated thread. All tasks which are scheduled to run after
 * this point are cancelled. Tasks are executed sequentially but are subject to
 * the delays from other tasks run methods. If a specific task takes an
 * excessive amount of time to run it may impact the time at which subsequent
 * tasks may run.
 * <p>
 *
 * The {@code TimerTask} does not offer any guarantees about the real-time nature of
 * scheduling tasks as its underlying implementation relies on the
 * {@code Object.wait(long)} method.
 * <p>
 * Multiple threads can share a single {@code Timer} without the need for their own
 * synchronization.
 * <p>
 * A {@code Timer} can be set to schedule tasks either at a fixed rate or
 * with a fixed period. Fixed-period execution is the default.
 * <p>
 * The difference between fixed-rate and fixed-period execution
 * is the following:  With fixed-rate execution, the start time of each
 * successive run of the task is scheduled in absolute terms without regard for when the previous
 * task run actually took place. This can result in a series of bunched-up runs
 * (one launched immediately after another) if busy resources or other
 * system delays prevent the {@code Timer} from firing for an extended time.
 * With fixed-period execution, each successive run of the
 * task is scheduled relative to the start time of the previous run of the
 * task, so two runs of the task are never fired closer together in time than
 * the specified {@code period}.
 *
 * @see TimerTask
 * @see java.lang.Object#wait(long)
 */
public class Timer
{

    private static final class TimerImpl extends Thread
    {

        private static final class TimerHeap
        {
            private int DEFAULT_HEAP_SIZE = 256;

            private TimerTask[] timers = new TimerTask[DEFAULT_HEAP_SIZE];

            private int size = 0;

            private int deletedCancelledNumber = 0;

            public TimerTask minimum()
            {
                return timers[0];
            }

            public boolean isEmpty()
            {
                return size == 0;
            }

            public void insert(TimerTask task)
            {
                if (timers.length == size)
                {
                    TimerTask[] appendedTimers = new TimerTask[size * 2];
                    System.arraycopy(timers, 0, appendedTimers, 0, size);
                    timers = appendedTimers;
                }
                timers[size++] = task;
                upHeap();
            }

            public void delete(int pos)
            {
                // posible to delete any position of the heap
                if (pos >= 0 && pos < size)
                {
                    timers[pos] = timers[--size];
                    timers[size] = null;
                    downHeap(pos);
                }
            }

            private void upHeap()
            {
                int current = size - 1;
                int parent = (current - 1) / 2;

                while (timers[current].when < timers[parent].when)
                {
                    // swap the two
                    TimerTask tmp = timers[current];
                    timers[current] = timers[parent];
                    timers[parent] = tmp;

                    // update pos and current
                    current = parent;
                    parent = (current - 1) / 2;
                }
            }

            private void downHeap(int pos)
            {
                int current = pos;
                int child = 2 * current + 1;

                while (child < size && size > 0)
                {
                    // compare the children if they exist
                    if (child + 1 < size
                            && timers[child + 1].when < timers[child].when)
                    {
                        child++;
                    }

                    // compare selected child with parent
                    if (timers[current].when < timers[child].when)
                    {
                        break;
                    }

                    // swap the two
                    TimerTask tmp = timers[current];
                    timers[current] = timers[child];
                    timers[child] = tmp;

                    // update pos and current
                    current = child;
                    child = 2 * current + 1;
                }
            }

            public void reset()
            {
                timers = new TimerTask[DEFAULT_HEAP_SIZE];
                size = 0;
            }

            public void adjustMinimum()
            {
                downHeap(0);
            }

            public void deleteIfCancelled()
            {
                for (int i = 0; i < size; i++)
                {
                    if (timers[i].cancelled)
                    {
                        deletedCancelledNumber++;
                        delete(i);
                        // re-try this point
                        i--;
                    }
                }
            }

            private int getTask(TimerTask task)
            {
                for (int i = 0; i < timers.length; i++)
                {
                    if (timers[i] == task)
                    {
                        return i;
                    }
                }
                return -1;
            }

        }

        /**
         * True if the method cancel() of the Timer was called or the !!!stop()
         * method was invoked
         */
        private boolean cancelled;

        /**
         * True if the Timer has become garbage
         */
        private boolean finished;

        /**
         * Vector consists of scheduled events, sorted according to
         * {@code when} field of TaskScheduled object.
         */
        private TimerHeap tasks = new TimerHeap();

        /**
         * Starts a new timer.
         *
         * @param name thread's name
         * @param isDaemon daemon thread or not
         */
        TimerImpl(String name, boolean isDaemon)
        {
            // not supported in Java ME
            /*
            this.setName(name);
            this.setDaemon(isDaemon);
            */
            this.start();
        }

        /**
         * This method will be launched on separate thread for each Timer
         * object.
         */
        //@Override
        public void run()
        {
            while (true)
            {
                TimerTask task;
                synchronized (this)
                {
                    // need to check cancelled inside the synchronized block
                    if (cancelled)
                    {
                        return;
                    }
                    if (tasks.isEmpty())
                    {
                        if (finished)
                        {
                            return;
                        }
                        // no tasks scheduled -- sleep until any task appear
                        try
                        {
                            this.wait();
                        }
                        catch (InterruptedException e)
                        {
                        }
                        continue;
                    }

                    long currentTime = System.currentTimeMillis();

                    task = tasks.minimum();
                    long timeToSleep;

                    synchronized (task.lock)
                    {
                        if (task.cancelled)
                        {
                            tasks.delete(0);
                            continue;
                        }

                        // check the time to sleep for the first task scheduled
                        timeToSleep = task.when - currentTime;
                    }

                    if (timeToSleep > 0)
                    {
                        // sleep!
                        try
                        {
                            this.wait(timeToSleep);
                        }
                        catch (InterruptedException e)
                        {
                            // Ignored
                        }
                        continue;
                    }

                    // no sleep is necessary before launching the task

                    synchronized (task.lock)
                    {
                        int pos = 0;
                        if (tasks.minimum().when != task.when)
                        {
                            pos = tasks.getTask(task);
                        }
                        if (task.cancelled)
                        {
                            tasks.delete(tasks.getTask(task));
                            continue;
                        }

                        // set time to schedule
                        task.setScheduledTime(task.when);

                        // remove task from queue
                        tasks.delete(pos);

                        // set when the next task should be launched
                        if (task.period >= 0)
                        {
                            // this is a repeating task,
                            if (task.fixedRate)
                            {
                                // task is scheduled at fixed rate
                                task.when = task.when + task.period;
                            }
                            else
                            {
                                // task is scheduled at fixed delay
                                task.when = System.currentTimeMillis()
                                            + task.period;
                            }

                            // insert this task into queue
                            insertTask(task);
                        }
                        else
                        {
                            task.when = 0;
                        }
                    }
                }

                // run the task
                task.run(); // we want that unhandled exception goes through
                /*
                                try {
                                    task.run();
                                } catch (Exception e) {
                                    // Ignored
                                }
                */
            }
        }

        private void insertTask(TimerTask newTask)
        {
            // callers are synchronized
            tasks.insert(newTask);
            this.notify();
        }

        /**
         * Cancels timer.
         */
        public synchronized void cancel()
        {
            cancelled = true;
            tasks.reset();
            this.notify();
        }

        public int purge()
        {
            if (tasks.isEmpty())
            {
                return 0;
            }
            // callers are synchronized
            tasks.deletedCancelledNumber = 0;
            tasks.deleteIfCancelled();
            return tasks.deletedCancelledNumber;
        }

    }

    // Used to finalize thread
    private Finalizer finalizer = new Finalizer()
    {
        public void finalizeImpl()
        {
            doFinalize();
        }
    };
    private void doFinalize()
    {
        synchronized (impl)
        {
            impl.finished = true;
            impl.notify();
        }
    }

    private static long timerId;

    private synchronized static long nextId()
    {
        return timerId++;
    }

    /* This object will be used in synchronization purposes */
    private final TimerImpl impl;

    /**
     * Creates a new named {@code Timer} which may be specified to be run as a
     * daemon thread.
     *
     * @param name the name of the {@code Timer}.
     * @param isDaemon true if {@code Timer}'s thread should be a daemon thread.
     * @throws NullPointerException is {@code name} is {@code null}
     */
    private Timer(String name, boolean isDaemon)
    {
        super();
        if (name == null)
        {
            throw new NullPointerException("name is null");
        }
        this.impl = new TimerImpl(name, isDaemon);
    }

    /**
     * Creates a new named {@code Timer} which does not run as a daemon thread.
     *
     * @param name the name of the Timer.
     * @throws NullPointerException is {@code name} is {@code null}
     */
    private Timer(String name)
    {
        this(name, false);
    }

    /**
     * Creates a new {@code Timer} which may be specified to be run as a daemon thread.
     *
     * @param isDaemon {@code true} if the {@code Timer}'s thread should be a daemon thread.
     */
    private Timer(boolean isDaemon)
    {
        this("Timer-" + Timer.nextId(), isDaemon);
    }

    /**
     * Creates a new non-daemon {@code Timer}.
     */
    public Timer()
    {
        this(false);
    }

    /**
     * Cancels the {@code Timer} and removes any scheduled tasks. If there is a
     * currently running task it is not affected. No more tasks may be scheduled
     * on this {@code Timer}. Subsequent calls do nothing.
     */
    public void cancel()
    {
        impl.cancel();
    }

    /**
     * Removes all canceled tasks from the task queue. If there are no
     * other references on the tasks, then after this call they are free
     * to be garbage collected.
     *
     * @return the number of canceled tasks that were removed from the task
     *         queue.
     */
    private int purge()
    {
        synchronized (impl)
        {
            return impl.purge();
        }
    }

    /**
     * Schedule a task for single execution. If {@code when} is less than the
     * current time, it will be scheduled to be executed as soon as possible.
     *
     * @param task
     *            the task to schedule.
     * @param when
     *            time of execution.
     * @throws IllegalArgumentException
     *                if {@code when.getTime() < 0}.
     * @throws IllegalStateException
     *                if the {@code Timer} has been canceled, or if the task has been
     *                scheduled or canceled.
     */
    public void schedule(TimerTask task, Date when)
    {
        if (when.getTime() < 0)
        {
            throw new IllegalArgumentException();
        }
        long delay = when.getTime() - System.currentTimeMillis();
        scheduleImpl(task, delay < 0 ? 0 : delay, -1, false);
    }

    /**
     * Schedule a task for single execution after a specified delay.
     *
     * @param task
     *            the task to schedule.
     * @param delay
     *            amount of time in milliseconds before execution.
     * @throws IllegalArgumentException
     *                if {@code delay < 0}.
     * @throws IllegalStateException
     *                if the {@code Timer} has been canceled, or if the task has been
     *                scheduled or canceled.
     */
    public void schedule(TimerTask task, long delay)
    {
        if (delay < 0)
        {
            throw new IllegalArgumentException();
        }
        scheduleImpl(task, delay, -1, false);
    }

    /**
     * Schedule a task for repeated fixed-delay execution after a specific delay.
     *
     * @param task
     *            the task to schedule.
     * @param delay
     *            amount of time in milliseconds before first execution.
     * @param period
     *            amount of time in milliseconds between subsequent executions.
     * @throws IllegalArgumentException
     *                if {@code delay < 0} or {@code period < 0}.
     * @throws IllegalStateException
     *                if the {@code Timer} has been canceled, or if the task has been
     *                scheduled or canceled.
     */
    public void schedule(TimerTask task, long delay, long period)
    {
        if (delay < 0 || period <= 0)
        {
            throw new IllegalArgumentException();
        }
        scheduleImpl(task, delay, period, false);
    }

    /**
     * Schedule a task for repeated fixed-delay execution after a specific time
     * has been reached.
     *
     * @param task
     *            the task to schedule.
     * @param when
     *            time of first execution.
     * @param period
     *            amount of time in milliseconds between subsequent executions.
     * @throws IllegalArgumentException
     *                if {@code when.getTime() < 0} or {@code period < 0}.
     * @throws IllegalStateException
     *                if the {@code Timer} has been canceled, or if the task has been
     *                scheduled or canceled.
     */
    public void schedule(TimerTask task, Date when, long period)
    {
        if (period <= 0 || when.getTime() < 0)
        {
            throw new IllegalArgumentException();
        }
        long delay = when.getTime() - System.currentTimeMillis();
        scheduleImpl(task, delay < 0 ? 0 : delay, period, false);
    }

    /**
     * Schedule a task for repeated fixed-rate execution after a specific delay
     * has passed.
     *
     * @param task
     *            the task to schedule.
     * @param delay
     *            amount of time in milliseconds before first execution.
     * @param period
     *            amount of time in milliseconds between subsequent executions.
     * @throws IllegalArgumentException
     *                if {@code delay < 0} or {@code period < 0}.
     * @throws IllegalStateException
     *                if the {@code Timer} has been canceled, or if the task has been
     *                scheduled or canceled.
     */
    public void scheduleAtFixedRate(TimerTask task, long delay, long period)
    {
        if (delay < 0 || period <= 0)
        {
            throw new IllegalArgumentException();
        }
        scheduleImpl(task, delay, period, true);
    }

    /**
     * Schedule a task for repeated fixed-rate execution after a specific time
     * has been reached.
     *
     * @param task
     *            the task to schedule.
     * @param when
     *            time of first execution.
     * @param period
     *            amount of time in milliseconds between subsequent executions.
     * @throws IllegalArgumentException
     *                if {@code when.getTime() < 0} or {@code period < 0}.
     * @throws IllegalStateException
     *                if the {@code Timer} has been canceled, or if the task has been
     *                scheduled or canceled.
     */
    public void scheduleAtFixedRate(TimerTask task, Date when, long period)
    {
        if (period <= 0 || when.getTime() < 0)
        {
            throw new IllegalArgumentException();
        }
        long delay = when.getTime() - System.currentTimeMillis();
        scheduleImpl(task, delay < 0 ? 0 : delay, period, true);
    }

    /*
     * Schedule a task.
     */
    private void scheduleImpl(TimerTask task, long delay, long period,
                              boolean fixed)
    {
        synchronized (impl)
        {
            if (impl.cancelled)
            {
                throw new IllegalStateException("Timer is cancelled");
            }

            long when = delay + System.currentTimeMillis();

            if (when < 0)
            {
                throw new IllegalArgumentException("Illegal delay to start TimerTask");
            }

            synchronized (task.lock)
            {
                if (task.isScheduled())
                {
                    throw new IllegalStateException("TimerTask is already scheduled");
                }

                if (task.cancelled)
                {
                    throw new IllegalStateException("TimerTask is cancelled");
                }

                task.when = when;
                task.period = period;
                task.fixedRate = fixed;
            }

            // insert the newTask into queue
            impl.insertTask(task);
        }
    }
}
