/*
 * Copyright (C) 2014 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.telecomm;

import android.net.Uri;
import android.os.Bundle;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Represents a connection to a remote endpoint that carries voice traffic.
 */
public abstract class Connection {

    public interface Listener {
        void onStateChanged(Connection c, int state);
        void onAudioStateChanged(Connection c, CallAudioState state);
        void onHandleChanged(Connection c, Uri newHandle);
        void onSignalChanged(Connection c, Bundle details);
        void onDisconnected(Connection c, int cause, String message);
        void onRequestingRingback(Connection c, boolean ringback);
        void onDestroyed(Connection c);
        void onConferenceCapableChanged(Connection c, boolean isConferenceCapable);
        void onParentConnectionChanged(Connection c, Connection parent);
    }

    public static class ListenerBase implements Listener {
        /** {@inheritDoc} */
        @Override
        public void onStateChanged(Connection c, int state) {}

        /** {@inheritDoc} */
         @Override
        public void onAudioStateChanged(Connection c, CallAudioState state) {}

        /** {@inheritDoc} */
        @Override
        public void onHandleChanged(Connection c, Uri newHandle) {}

        /** {@inheritDoc} */
        @Override
        public void onSignalChanged(Connection c, Bundle details) {}

        /** {@inheritDoc} */
        @Override
        public void onDisconnected(Connection c, int cause, String message) {}

        /** {@inheritDoc} */
        @Override
        public void onDestroyed(Connection c) {}

        /** {@inheritDoc} */
        @Override
        public void onRequestingRingback(Connection c, boolean ringback) {}

        /** ${inheritDoc} */
        @Override
        public void onConferenceCapableChanged(Connection c, boolean isConferenceCapable) {}

        /** ${inheritDoc} */
        @Override
        public void onParentConnectionChanged(Connection c, Connection parent) {}
    }

    public final class State {
        private State() {}

        public static final int NEW = 0;
        public static final int RINGING = 1;
        public static final int DIALING = 2;
        public static final int ACTIVE = 3;
        public static final int HOLDING = 4;
        public static final int DISCONNECTED = 5;
    }

    private final Set<Listener> mListeners = new HashSet<>();
    private final List<Connection> mChildConnections = new ArrayList<>();

    private int mState = State.NEW;
    private CallAudioState mCallAudioState;
    private Uri mHandle;
    private boolean mRequestingRingback = false;
    private boolean mIsConferenceCapable = false;
    private Connection mParentConnection;

    /**
     * Create a new Connection.
     */
    protected Connection() {}

    /**
     * The handle (e.g., phone number) to which this Connection is currently communicating.
     *
     * IMPORTANT: If an incoming connection has a phone number (or other handle) that the user
     * is not supposed to be able to see (e.g. it is PRESENTATION_RESTRICTED), then a compliant
     * ConnectionService implementation MUST NOT reveal this phone number and MUST return
     * {@code null} from this method.
     *
     * @return The handle (e.g., phone number) to which this Connection
     *         is currently communicating.
     */
    public final Uri getHandle() {
        return mHandle;
    }

    /**
     * @return The state of this Connection.
     *
     * @hide
     */
    public final int getState() {
        return mState;
    }

    /**
     * @return The audio state of the call, describing how its audio is currently
     *         being routed by the system. This is {@code null} if this Connection
     *         does not directly know about its audio state.
     */
    public final CallAudioState getCallAudioState() {
        return mCallAudioState;
    }

    /**
     * Assign a listener to be notified of state changes.
     *
     * @param l A listener.
     * @return This Connection.
     *
     * @hide
     */
    public final Connection addConnectionListener(Listener l) {
        mListeners.add(l);
        return this;
    }

    /**
     * Remove a previously assigned listener that was being notified of state changes.
     *
     * @param l A Listener.
     * @return This Connection.
     *
     * @hide
     */
    public final Connection removeConnectionListener(Listener l) {
        mListeners.remove(l);
        return this;
    }

    /**
     * Play a DTMF tone in this Connection.
     *
     * @param c A DTMF character.
     *
     * @hide
     */
    public final void playDtmfTone(char c) {
        Log.d(this, "playDtmfTone %c", c);
        onPlayDtmfTone(c);
    }

    /**
     * Stop any DTMF tones which may be playing in this Connection.
     *
     * @hide
     */
    public final void stopDtmfTone() {
        Log.d(this, "stopDtmfTone");
        onStopDtmfTone();
    }

    /**
     * Disconnect this Connection. If and when the Connection can comply with
     * this request, it will transition to the {@link State#DISCONNECTED}
     * state and notify its listeners.
     *
     * @hide
     */
    public final void disconnect() {
        Log.d(this, "disconnect");
        onDisconnect();
    }

    /**
     * Separates this Connection from a parent connection.
     *
     * @hide
     */
    public final void separate() {
        Log.d(this, "separate");
        onSeparate();
    }

    /**
     * Abort this Connection. The Connection will immediately transition to
     * the {@link State#DISCONNECTED} state, and send no notifications of this
     * or any other future events.
     *
     * @hide
     */
    public final void abort() {
        Log.d(this, "abort");
        onAbort();
    }

    /**
     * Place this Connection on hold. If and when the Connection can comply with
     * this request, it will transition to the {@link State#HOLDING}
     * state and notify its listeners.
     *
     * @hide
     */
    public final void hold() {
        Log.d(this, "hold");
        onHold();
    }

    /**
     * Un-hold this Connection. If and when the Connection can comply with
     * this request, it will transition to the {@link State#ACTIVE}
     * state and notify its listeners.
     *
     * @hide
     */
    public final void unhold() {
        Log.d(this, "unhold");
        onUnhold();
    }

    /**
     * Accept a {@link State#RINGING} Connection. If and when the Connection
     * can comply with this request, it will transition to the {@link State#ACTIVE}
     * state and notify its listeners.
     *
     * @hide
     */
    public final void answer() {
        Log.d(this, "answer");
        if (mState == State.RINGING) {
            onAnswer();
        }
    }

    /**
     * Reject a {@link State#RINGING} Connection. If and when the Connection
     * can comply with this request, it will transition to the {@link State#ACTIVE}
     * state and notify its listeners.
     *
     * @hide
     */
    public final void reject() {
        Log.d(this, "reject");
        if (mState == State.RINGING) {
            onReject();
        }
    }

    /**
     * TODO(santoscordon): Needs updated documentation.
     */
    public final void conference() {
        Log.d(this, "conference");
        onConference();
    }

    /**
     * Inform this Connection that the state of its audio output has been changed externally.
     *
     * @param state The new audio state.
     */
    public void setAudioState(CallAudioState state) {
        Log.d(this, "setAudioState %s", state);
        onSetAudioState(state);
    }

    /**
     * @param state An integer value from {@link State}.
     * @return A string representation of the value.
     */
    public static String stateToString(int state) {
        switch (state) {
            case State.NEW:
                return "NEW";
            case State.RINGING:
                return "RINGING";
            case State.DIALING:
                return "DIALING";
            case State.ACTIVE:
                return "ACTIVE";
            case State.HOLDING:
                return "HOLDING";
            case State.DISCONNECTED:
                return "DISCONNECTED";
            default:
                Log.wtf(Connection.class, "Unknown state %d", state);
                return "UNKNOWN";
        }
    }

    /**
     * Returns whether this connection is requesting that the system play a ringback tone
     * on its behalf.
     */
    public boolean isRequestingRingback() {
        return mRequestingRingback;
    }

    /**
     * Returns whether this connection is a conference connection (has child connections).
     */
    public boolean isConferenceConnection() {
        return !mChildConnections.isEmpty();
    }

    public void setParentConnection(Connection parentConnection) {
        Log.d(this, "parenting %s to %s", this, parentConnection);
        if (mParentConnection != parentConnection) {
            if (mParentConnection != null) {
                mParentConnection.removeChild(this);
            }
            mParentConnection = parentConnection;
            if (mParentConnection != null) {
                mParentConnection.addChild(this);
                // do something if the child connections goes down to ZERO.
            }
            for (Listener l : mListeners) {
                l.onParentConnectionChanged(this, mParentConnection);
            }
        }
    }

    public Connection getParentConnection() {
        return mParentConnection;
    }

    public List<Connection> getChildConnections() {
        return mChildConnections;
    }

    /**
     * Returns whether this connection is capable of being conferenced.
     */
    public boolean isConferenceCapable() {
        return mIsConferenceCapable;
    }

    /**
     * Sets the value of the {@link #getHandle()} property and notifies listeners.
     *
     * @param handle The new handle.
     */
    protected void setHandle(Uri handle) {
        Log.d(this, "setHandle %s", handle);
        // TODO: Enforce super called
        mHandle = handle;
        for (Listener l : mListeners) {
            l.onHandleChanged(this, handle);
        }
    }

    /**
     * Sets state to active (e.g., an ongoing call where two or more parties can actively
     * communicate).
     */
    protected void setActive() {
        setRequestingRingback(false);
        setState(State.ACTIVE);
    }

    /**
     * Sets state to ringing (e.g., an inbound ringing call).
     */
    protected void setRinging() {
        setState(State.RINGING);
    }

    /**
     * Sets state to dialing (e.g., dialing an outbound call).
     */
    protected void setDialing() {
        setState(State.DIALING);
    }

    /**
     * Sets state to be on hold.
     */
    protected void setOnHold() {
        setState(State.HOLDING);
    }

    /**
     * Sets state to disconnected. This will first notify listeners with an
     * {@link Listener#onStateChanged(Connection, int)} event, then will fire an
     * {@link Listener#onDisconnected(Connection, int, String)} event with additional
     * details.
     *
     * @param cause The reason for the disconnection, any of
     *         {@link android.telephony.DisconnectCause}.
     * @param message Optional call-service-provided message about the disconnect.
     */
    protected void setDisconnected(int cause, String message) {
        setState(State.DISCONNECTED);
        Log.d(this, "Disconnected with cause %d message %s", cause, message);
        for (Listener l : mListeners) {
            l.onDisconnected(this, cause, message);
        }
    }

    /**
     * Requests that the framework play a ringback tone. This is to be invoked by implementations
     * that do not play a ringback tone themselves in the call's audio stream.
     *
     * @param ringback Whether the ringback tone is to be played.
     */
    protected void setRequestingRingback(boolean ringback) {
        if (mRequestingRingback != ringback) {
            mRequestingRingback = ringback;
            for (Listener l : mListeners) {
                l.onRequestingRingback(this, ringback);
            }
        }
    }

    /**
     * TODO(santoscordon): Needs documentation.
     */
    protected void setIsConferenceCapable(boolean isConferenceCapable) {
        if (mIsConferenceCapable != isConferenceCapable) {
            mIsConferenceCapable = isConferenceCapable;
            for (Listener l : mListeners) {
                l.onConferenceCapableChanged(this, mIsConferenceCapable);
            }
        }
    }

    /**
     * TODO(santoscordon): Needs documentation.
     */
    protected void setDestroyed() {
        // It is possible that onDestroy() will trigger the listener to remove itself which will
        // result in a concurrent modification exception. To counteract this we make a copy of the
        // listeners and iterate on that.
        for (Listener l : new ArrayList<>(mListeners)) {
            if (mListeners.contains(l)) {
                l.onDestroyed(this);
            }
        }
    }

    /**
     * Notifies this Connection and listeners that the {@link #getCallAudioState()} property
     * has a new value.
     *
     * @param state The new call audio state.
     */
    protected void onSetAudioState(CallAudioState state) {
        // TODO: Enforce super called
        mCallAudioState = state;
        for (Listener l : mListeners) {
            l.onAudioStateChanged(this, state);
        }
    }

    /**
     * Notifies this Connection and listeners of a change in the current signal levels
     * for the underlying data transport.
     *
     * @param details A {@link android.os.Bundle} containing details of the current level.
     */
    protected void onSetSignal(Bundle details) {
        // TODO: Enforce super called
        for (Listener l : mListeners) {
            l.onSignalChanged(this, details);
        }
    }

    /**
     * Notifies this Connection of an internal state change. This method is called before the
     * state is actually changed. Overriding implementations must call
     * {@code super.onSetState(state)}.
     *
     * @param state The new state, a {@link Connection.State} member.
     */
    protected void onSetState(int state) {
        // TODO: Enforce super called
        this.mState = state;
        for (Listener l : mListeners) {
            l.onStateChanged(this, state);
        }
    }

    /**
     * Notifies this Connection of a request to play a DTMF tone.
     *
     * @param c A DTMF character.
     */
    protected void onPlayDtmfTone(char c) {}

    /**
     * Notifies this Connection of a request to stop any currently playing DTMF tones.
     */
    protected void onStopDtmfTone() {}

    /**
     * Notifies this Connection of a request to disconnect.
     */
    protected void onDisconnect() {}

    /**
     * Notifies this Connection of a request to disconnect.
     */
    protected void onSeparate() {}

    /**
     * Notifies this Connection of a request to abort.
     */
    protected void onAbort() {}

    /**
     * Notifies this Connection of a request to hold.
     */
    protected void onHold() {}

    /**
     * Notifies this Connection of a request to exit a hold state.
     */
    protected void onUnhold() {}

    /**
     * Notifies this Connection, which is in {@link State#RINGING}, of
     * a request to accept.
     */
    protected void onAnswer() {}

    /**
     * Notifies this Connection, which is in {@link State#RINGING}, of
     * a request to reject.
     */
    protected void onReject() {}

    /**
     * Notifies this Connection whether the user wishes to proceed with the post-dial DTMF codes.
     */
    protected void onPostDialContinue(boolean proceed) {}

    /**
     * TODO(santoscordon): Needs documentation.
     */
    protected void onConference() {}

    /**
     * TODO(santoscordon): Needs documentation.
     */
    protected void onChildrenChanged(List<Connection> children) {}

    private void addChild(Connection connection) {
        Log.d(this, "adding child %s", connection);
        mChildConnections.add(connection);
        onChildrenChanged(mChildConnections);
    }

    private void removeChild(Connection connection) {
        Log.d(this, "removing child %s", connection);
        mChildConnections.remove(connection);
        onChildrenChanged(mChildConnections);
    }

    private void setState(int state) {
        Log.d(this, "setState: %s", stateToString(state));
        onSetState(state);
    }
}
