/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * 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
 *
 *      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.
 */
package android.os;

import java.net.InetSocketAddress;
import java.util.NoSuchElementException;
import android.os.Binder;
import android.os.CommonTimeUtils;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.ServiceManager;

/**
 * Used for accessing the android common time service's common clock and receiving notifications
 * about common time synchronization status changes.
 * @hide
 */
public class CommonClock {
    /**
     * Sentinel value returned by {@link #getTime()} and {@link #getEstimatedError()} when the
     * common time service is not able to determine the current common time due to a lack of
     * synchronization.
     */
    public static final long TIME_NOT_SYNCED = -1;

    /**
     * Sentinel value returned by {@link #getTimelineId()} when the common time service is not
     * currently synced to any timeline.
     */
    public static final long INVALID_TIMELINE_ID = 0;

    /**
     * Sentinel value returned by {@link #getEstimatedError()} when the common time service is not
     * currently synced to any timeline.
     */
    public static final int ERROR_ESTIMATE_UNKNOWN = 0x7FFFFFFF;

    /**
     * Value used by {@link #getState()} to indicate that there was an internal error while
     * attempting to determine the state of the common time service.
     */
    public static final int STATE_INVALID = -1;

    /**
     * Value used by {@link #getState()} to indicate that the common time service is in its initial
     * state and attempting to find the current timeline master, if any.  The service will
     * transition to either {@link #STATE_CLIENT} if it finds an active master, or to
     * {@link #STATE_MASTER} if no active master is found and this client becomes the master of a
     * new timeline.
     */
    public static final int STATE_INITIAL = 0;

    /**
     * Value used by {@link #getState()} to indicate that the common time service is in its client
     * state and is synchronizing its time to a different timeline master on the network.
     */
    public static final int STATE_CLIENT = 1;

    /**
     * Value used by {@link #getState()} to indicate that the common time service is in its master
     * state and is serving as the timeline master for other common time service clients on the
     * network.
     */
    public static final int STATE_MASTER = 2;

    /**
     * Value used by {@link #getState()} to indicate that the common time service is in its Ronin
     * state.  Common time service instances in the client state enter the Ronin state after their
     * timeline master becomes unreachable on the network.  Common time services who enter the Ronin
     * state will begin a new master election for the timeline they were recently clients of.  As
     * clients detect they are not the winner and drop out of the election, they will transition to
     * the {@link #STATE_WAIT_FOR_ELECTION} state.  When there is only one client remaining in the
     * election, it will assume ownership of the timeline and transition to the
     * {@link #STATE_MASTER} state.  During the election, all clients will allow their timeline to
     * drift without applying correction.
     */
    public static final int STATE_RONIN = 3;

    /**
     * Value used by {@link #getState()} to indicate that the common time service is waiting for a
     * master election to conclude and for the new master to announce itself before transitioning to
     * the {@link #STATE_CLIENT} state.  If no new master announces itself within the timeout
     * threshold, the time service will transition back to the {@link #STATE_RONIN} state in order
     * to restart the election.
     */
    public static final int STATE_WAIT_FOR_ELECTION = 4;

    /**
     * Name of the underlying native binder service
     */
    public static final String SERVICE_NAME = "common_time.clock";

    /**
     * Class constructor.
     * @throws android.os.RemoteException
     */
    public CommonClock()
    throws RemoteException {
        mRemote = ServiceManager.getService(SERVICE_NAME);
        if (null == mRemote)
            throw new RemoteException();

        mInterfaceDesc = mRemote.getInterfaceDescriptor();
        mUtils = new CommonTimeUtils(mRemote, mInterfaceDesc);
        mRemote.linkToDeath(mDeathHandler, 0);
        registerTimelineChangeListener();
    }

    /**
     * Handy class factory method.
     */
    static public CommonClock create() {
        CommonClock retVal;

        try {
            retVal = new CommonClock();
        }
        catch (RemoteException e) {
            retVal = null;
        }

        return retVal;
    }

    /**
     * Release all native resources held by this {@link android.os.CommonClock} instance.  Once
     * resources have been released, the {@link android.os.CommonClock} instance is disconnected from
     * the native service and will throw a {@link android.os.RemoteException} if any of its
     * methods are called.  Clients should always call release on their client instances before
     * releasing their last Java reference to the instance.  Failure to do this will cause
     * non-deterministic native resource reclamation and may cause the common time service to remain
     * active on the network for longer than it should.
     */
    public void release() {
        unregisterTimelineChangeListener();
        if (null != mRemote) {
            try {
                mRemote.unlinkToDeath(mDeathHandler, 0);
            }
            catch (NoSuchElementException e) { }
            mRemote = null;
        }
        mUtils = null;
    }

    /**
     * Gets the common clock's current time.
     *
     * @return a signed 64-bit value representing the current common time in microseconds, or the
     * special value {@link #TIME_NOT_SYNCED} if the common time service is currently not
     * synchronized.
     * @throws android.os.RemoteException
     */
    public long getTime()
    throws RemoteException {
        throwOnDeadServer();
        return mUtils.transactGetLong(METHOD_GET_COMMON_TIME, TIME_NOT_SYNCED);
    }

    /**
     * Gets the current estimation of common clock's synchronization accuracy from the common time
     * service.
     *
     * @return a signed 32-bit value representing the common time service's estimation of
     * synchronization accuracy in microseconds, or the special value
     * {@link #ERROR_ESTIMATE_UNKNOWN} if the common time service is currently not synchronized.
     * Negative values indicate that the local server estimates that the nominal common time is
     * behind the local server's time (in other words, the local clock is running fast) Positive
     * values indicate that the local server estimates that the nominal common time is ahead of the
     * local server's time (in other words, the local clock is running slow)
     * @throws android.os.RemoteException
     */
    public int getEstimatedError()
    throws RemoteException {
        throwOnDeadServer();
        return mUtils.transactGetInt(METHOD_GET_ESTIMATED_ERROR, ERROR_ESTIMATE_UNKNOWN);
    }

    /**
     * Gets the ID of the timeline the common time service is currently synchronizing its clock to.
     *
     * @return a long representing the unique ID of the timeline the common time service is
     * currently synchronizing with, or {@link #INVALID_TIMELINE_ID} if the common time service is
     * currently not synchronized.
     * @throws android.os.RemoteException
     */
    public long getTimelineId()
    throws RemoteException {
        throwOnDeadServer();
        return mUtils.transactGetLong(METHOD_GET_TIMELINE_ID, INVALID_TIMELINE_ID);
    }

    /**
     * Gets the current state of this clock's common time service in the the master election
     * algorithm.
     *
     * @return a integer indicating the current state of the this clock's common time service in the
     * master election algorithm or {@link #STATE_INVALID} if there is an internal error.
     * @throws android.os.RemoteException
     */
    public int getState()
    throws RemoteException {
        throwOnDeadServer();
        return mUtils.transactGetInt(METHOD_GET_STATE, STATE_INVALID);
    }

    /**
     * Gets the IP address and UDP port of the current timeline master.
     *
     * @return an InetSocketAddress containing the IP address and UDP port of the current timeline
     * master, or null if there is no current master.
     * @throws android.os.RemoteException
     */
    public InetSocketAddress getMasterAddr()
    throws RemoteException {
        throwOnDeadServer();
        return mUtils.transactGetSockaddr(METHOD_GET_MASTER_ADDRESS);
    }

    /**
     * The OnTimelineChangedListener interface defines a method called by the
     * {@link android.os.CommonClock} instance to indicate that the time synchronization service has
     * either synchronized with a new timeline, or is no longer a member of any timeline.  The
     * client application can implement this interface and register the listener with the
     * {@link #setTimelineChangedListener(OnTimelineChangedListener)} method.
     */
    public interface OnTimelineChangedListener  {
        /**
         * Method called when the time service's timeline has changed.
         *
         * @param newTimelineId a long which uniquely identifies the timeline the time
         * synchronization service is now a member of, or {@link #INVALID_TIMELINE_ID} if the the
         * service is not synchronized to any timeline.
         */
        void onTimelineChanged(long newTimelineId);
    }

    /**
     * Registers an OnTimelineChangedListener interface.
     * <p>Call this method with a null listener to stop receiving server death notifications.
     */
    public void setTimelineChangedListener(OnTimelineChangedListener listener) {
        synchronized (mListenerLock) {
            mTimelineChangedListener = listener;
        }
    }

    /**
     * The OnServerDiedListener interface defines a method called by the
     * {@link android.os.CommonClock} instance to indicate that the connection to the native media
     * server has been broken and that the {@link android.os.CommonClock} instance will need to be
     * released and re-created.  The client application can implement this interface and register
     * the listener with the {@link #setServerDiedListener(OnServerDiedListener)} method.
     */
    public interface OnServerDiedListener  {
        /**
         * Method called when the native media server has died.  <p>If the native common time
         * service encounters a fatal error and needs to restart, the binder connection from the
         * {@link android.os.CommonClock} instance to the common time service will be broken.  To
         * restore functionality, clients should {@link #release()} their old visualizer and create
         * a new instance.
         */
        void onServerDied();
    }

    /**
     * Registers an OnServerDiedListener interface.
     * <p>Call this method with a null listener to stop receiving server death notifications.
     */
    public void setServerDiedListener(OnServerDiedListener listener) {
        synchronized (mListenerLock) {
            mServerDiedListener = listener;
        }
    }

    protected void finalize() throws Throwable { release(); }

    private void throwOnDeadServer() throws RemoteException {
        if ((null == mRemote) || (null == mUtils))
            throw new RemoteException();
    }

    private final Object mListenerLock = new Object();
    private OnTimelineChangedListener mTimelineChangedListener = null;
    private OnServerDiedListener mServerDiedListener = null;

    private IBinder mRemote = null;
    private String mInterfaceDesc = "";
    private CommonTimeUtils mUtils;

    private IBinder.DeathRecipient mDeathHandler = new IBinder.DeathRecipient() {
        public void binderDied() {
            synchronized (mListenerLock) {
                if (null != mServerDiedListener)
                    mServerDiedListener.onServerDied();
            }
        }
    };

    private class TimelineChangedListener extends Binder {
        @Override
        protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
        throws RemoteException {
            switch (code) {
                case METHOD_CBK_ON_TIMELINE_CHANGED:
                    data.enforceInterface(DESCRIPTOR);
                    long timelineId = data.readLong();
                    synchronized (mListenerLock) {
                        if (null != mTimelineChangedListener)
                            mTimelineChangedListener.onTimelineChanged(timelineId);
                    }
                    return true;
            }

            return super.onTransact(code, data, reply, flags);
        }

        private static final String DESCRIPTOR = "android.os.ICommonClockListener";
    };

    private TimelineChangedListener mCallbackTgt = null;

    private void registerTimelineChangeListener() throws RemoteException {
        if (null != mCallbackTgt)
            return;

        boolean success = false;
        android.os.Parcel data  = android.os.Parcel.obtain();
        android.os.Parcel reply = android.os.Parcel.obtain();
        mCallbackTgt = new TimelineChangedListener();

        try {
            data.writeInterfaceToken(mInterfaceDesc);
            data.writeStrongBinder(mCallbackTgt);
            mRemote.transact(METHOD_REGISTER_LISTENER, data, reply, 0);
            success = (0 == reply.readInt());
        }
        catch (RemoteException e) {
            success = false;
        }
        finally {
            reply.recycle();
            data.recycle();
        }

        // Did we catch a remote exception or fail to register our callback target?  If so, our
        // object must already be dead (or be as good as dead).  Clear out all of our state so that
        // our other methods will properly indicate a dead object.
        if (!success) {
            mCallbackTgt = null;
            mRemote = null;
            mUtils = null;
        }
    }

    private void unregisterTimelineChangeListener() {
        if (null == mCallbackTgt)
            return;

        android.os.Parcel data  = android.os.Parcel.obtain();
        android.os.Parcel reply = android.os.Parcel.obtain();

        try {
            data.writeInterfaceToken(mInterfaceDesc);
            data.writeStrongBinder(mCallbackTgt);
            mRemote.transact(METHOD_UNREGISTER_LISTENER, data, reply, 0);
        }
        catch (RemoteException e) { }
        finally {
            reply.recycle();
            data.recycle();
            mCallbackTgt = null;
        }
    }

    private static final int METHOD_IS_COMMON_TIME_VALID = IBinder.FIRST_CALL_TRANSACTION;
    private static final int METHOD_COMMON_TIME_TO_LOCAL_TIME = METHOD_IS_COMMON_TIME_VALID + 1;
    private static final int METHOD_LOCAL_TIME_TO_COMMON_TIME = METHOD_COMMON_TIME_TO_LOCAL_TIME + 1;
    private static final int METHOD_GET_COMMON_TIME = METHOD_LOCAL_TIME_TO_COMMON_TIME + 1;
    private static final int METHOD_GET_COMMON_FREQ = METHOD_GET_COMMON_TIME + 1;
    private static final int METHOD_GET_LOCAL_TIME = METHOD_GET_COMMON_FREQ + 1;
    private static final int METHOD_GET_LOCAL_FREQ = METHOD_GET_LOCAL_TIME + 1;
    private static final int METHOD_GET_ESTIMATED_ERROR = METHOD_GET_LOCAL_FREQ + 1;
    private static final int METHOD_GET_TIMELINE_ID = METHOD_GET_ESTIMATED_ERROR + 1;
    private static final int METHOD_GET_STATE = METHOD_GET_TIMELINE_ID + 1;
    private static final int METHOD_GET_MASTER_ADDRESS = METHOD_GET_STATE + 1;
    private static final int METHOD_REGISTER_LISTENER = METHOD_GET_MASTER_ADDRESS + 1;
    private static final int METHOD_UNREGISTER_LISTENER = METHOD_REGISTER_LISTENER + 1;

    private static final int METHOD_CBK_ON_TIMELINE_CHANGED = IBinder.FIRST_CALL_TRANSACTION;
}
