/*
 * TeleStax, Open Source Cloud Communications
 * Copyright 2011-2014, Telestax Inc and individual contributors
 * by the @authors tag.
 *
 * This program is free software: you can redistribute it and/or modify
 * under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */
package org.restcomm.connect.mgcp;

import static jain.protocol.ip.mgcp.message.parms.ReturnCode.Transaction_Executed_Normally;

import java.util.Map;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.StringUtils;
import org.mobicents.protocols.mgcp.jain.pkg.AUMgcpEvent;
import org.mobicents.protocols.mgcp.jain.pkg.AUPackage;
import org.restcomm.connect.commons.patterns.Observe;
import org.restcomm.connect.commons.patterns.StopObserving;

import akka.actor.ActorRef;
import jain.protocol.ip.mgcp.JainIPMgcpException;
import jain.protocol.ip.mgcp.JainMgcpResponseEvent;
import jain.protocol.ip.mgcp.message.NotificationRequest;
import jain.protocol.ip.mgcp.message.NotificationRequestResponse;
import jain.protocol.ip.mgcp.message.Notify;
import jain.protocol.ip.mgcp.message.NotifyResponse;
import jain.protocol.ip.mgcp.message.parms.EndpointIdentifier;
import jain.protocol.ip.mgcp.message.parms.EventName;
import jain.protocol.ip.mgcp.message.parms.NotifiedEntity;
import jain.protocol.ip.mgcp.message.parms.RequestIdentifier;
import jain.protocol.ip.mgcp.message.parms.RequestedAction;
import jain.protocol.ip.mgcp.message.parms.RequestedEvent;
import jain.protocol.ip.mgcp.message.parms.ReturnCode;
import jain.protocol.ip.mgcp.pkg.MgcpEvent;
import jain.protocol.ip.mgcp.pkg.PackageName;

/**
 * @author quintana.thomas@gmail.com (Thomas Quintana)
 * @author maria.farooq@telestax.com (Maria Farooq)
 */
public final class IvrEndpoint extends GenericEndpoint {
    private static final PackageName PACKAGE_NAME = AUPackage.AU;
    private static final RequestedEvent[] REQUESTED_EVENTS = new RequestedEvent[2];
    static {
        final RequestedAction[] action = new RequestedAction[] { RequestedAction.NotifyImmediately };
        REQUESTED_EVENTS[0] = new RequestedEvent(new EventName(PACKAGE_NAME, AUMgcpEvent.auoc), action);
        REQUESTED_EVENTS[1] = new RequestedEvent(new EventName(PACKAGE_NAME, AUMgcpEvent.auof), action);
    }
    private static final String EMPTY_STRING = new String();
    private static final String DEFAULT_REQUEST_ID = "0";

    private final NotifiedEntity agent;
    protected static String ivrEndpointName = "mobicents/ivr/$";

    public IvrEndpoint(final ActorRef gateway, final MediaSession session, final NotifiedEntity agent, final String domain, long timeout, String endpointName) {
        super(gateway, session, agent, new EndpointIdentifier(endpointName==null?ivrEndpointName:endpointName, domain), timeout);
        this.agent = agent;
    }

    private void sendAsr(final AsrSignal message) {
        MgcpEvent event = AsrSignal.REQUEST_ASR.withParm(message.toString());
        sendRequest(new EventName(PACKAGE_NAME, event), REQUESTED_EVENTS);
    }

    private void send(final Object message) {
        final Class<?> klass = message.getClass();
        final String parameters = message.toString();
        MgcpEvent event = null;
        if (Play.class.equals(klass)) {
            event = AUMgcpEvent.aupa.withParm(parameters);
        } else if (PlayCollect.class.equals(klass)) {
            event = AUMgcpEvent.aupc.withParm(parameters);
        } else if (PlayRecord.class.equals(klass)) {
            event = AUMgcpEvent.aupr.withParm(parameters);
        }
        sendRequest(new EventName(PACKAGE_NAME, event), REQUESTED_EVENTS);
    }

    private void sendRequest(EventName reqSignal, RequestedEvent[] reqEvents) {
        final EventName[] signal = new EventName[1];
        signal[0] = reqSignal;
        final RequestIdentifier requestId = new RequestIdentifier(DEFAULT_REQUEST_ID);
        final NotificationRequest request = new NotificationRequest(self(), id, requestId);
        request.setNotifiedEntity(agent);
        request.setRequestedEvents(reqEvents);
        request.setSignalRequests(signal);
        gateway.tell(request, self());
    }

    private void stop(Object message) {
        final EventName[] signal = new EventName[1];
        StopEndpoint se = (StopEndpoint) message;
        String parameters = "";
        if (se.getEvent() != null) {
            parameters = "sg=" + se.getEvent().getName();
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("StopEndpoint event is null");
            }
        }
        signal[0] = new EventName(PACKAGE_NAME, AUMgcpEvent.aues.withParm(parameters));
        final RequestIdentifier requestId = new RequestIdentifier(DEFAULT_REQUEST_ID);
        final NotificationRequest request = new NotificationRequest(self(), id, requestId);
        request.setSignalRequests(signal);
        request.setNotifiedEntity(agent);

        //Ask RMS to notify for PlayRecording and PlayAnnouncement Signal.
        //This way IVR endpoint state at RMS side, will be ready for any subsequent request
        if (se.getEvent().getName().equalsIgnoreCase("pr") || se.getEvent().getName().equalsIgnoreCase("pa") ) {
            request.setRequestedEvents(REQUESTED_EVENTS);
        }
        gateway.tell(request, self());
    }

    @Override
    public void onReceive(final Object message) throws Exception {
        final Class<?> klass = message.getClass();
        final ActorRef self = self();
        final ActorRef sender = sender();

        if (Observe.class.equals(klass)) {
            onObserve((Observe) message, self, sender);
        } else if (StopObserving.class.equals(klass)) {
            onStopObserving((StopObserving) message, self, sender);
        } else if (InviteEndpoint.class.equals(klass)) {
            final EndpointCredentials credentials = new EndpointCredentials(id);
            sender.tell(credentials, self);
        } else if (UpdateEndpointId.class.equals(klass)) {
            final UpdateEndpointId request = (UpdateEndpointId) message;
            id = request.id();
        } else if (DestroyEndpoint.class.equals(klass)) {
            onDestroyEndpoint((DestroyEndpoint) message, self, sender);
        } else if (Play.class.equals(klass) || PlayCollect.class.equals(klass) || PlayRecord.class.equals(klass)) {
            send(message);
        } else if (AsrSignal.class.equals(klass)) {
            sendAsr((AsrSignal) message);
        } else if (StopEndpoint.class.equals(klass)) {
            stop(message);
        } else if (Notify.class.equals(klass)) {
            notification(message);
        } else if (NotificationRequestResponse.class.equals(klass)) {
            response(message);
        } else if (message instanceof JainMgcpResponseEvent) {
            onJainMgcpResponseEvent((JainMgcpResponseEvent) message, self, sender);
        }
    }

    private void fail(final int code) {
        // Notify observers that the event failed.
        final ActorRef self = self();
        final String error = Integer.toString(code);
        final String message = "The IVR request failed with the following error code " + error;
        final JainIPMgcpException exception = new JainIPMgcpException(message);
        final IvrEndpointResponse response = new IvrEndpointResponse(exception);
        for (final ActorRef observer : observers) {
            observer.tell(response, self);
        }
    }

    private void response(final Object message) {
        final NotificationRequestResponse response = (NotificationRequestResponse) message;
        final ReturnCode code = response.getReturnCode();
        if (!Transaction_Executed_Normally.equals(code)) {
            final int value = code.getValue();
            fail(value);
        }
    }

    private void handleAsrr(final Map<String, String> parameters, int code) {
        if (parameters.containsKey("asrr")) {
            String asrr = parameters.get("asrr");
            if (!StringUtils.isEmpty(asrr)) {
                try {
                    asrr = new String(Hex.decodeHex(asrr.toCharArray()));
                } catch (DecoderException e) {
                    logger.error("asrr parameter cannot be decoded.", e);
                    fail(code);
                }
            }
            // Notify the observers that the event successfully completed.
            final IvrEndpointResponse result = new IvrEndpointResponse(new CollectedResult(asrr, true, code == 101));
            for (final ActorRef observer : observers) {
                observer.tell(result, self());
            }
        } else {
            logger.error("asrr parameter is missing");
            fail(code);
        }
    }

    private void notification(final Object message) {
        final Notify notification = (Notify) message;
        final ActorRef self = self();
        // Let the media server know we successfully got the notification.
        final NotifyResponse response = new NotifyResponse(this, Transaction_Executed_Normally);
        final int transaction = notification.getTransactionHandle();
        response.setTransactionHandle(transaction);
        gateway.tell(response, self);
        // We are only expecting one event "operation completed" or "operation failed".
        final EventName[] observedEvents = notification.getObservedEvents();
        if (observedEvents.length == 1) {
            final MgcpEvent event = observedEvents[0].getEventIdentifier();
            final Map<String, String> parameters = MgcpUtil.parseParameters(event.getParms());
            final int code = Integer.parseInt(parameters.get("rc"));
            switch (code) {
                case 323: // provisioning error
                case 326: // No digits
                case 327: // No speech
                case 328: // Spoke too long
                case 329: // Digit pattern not matched
                case 331: // Speech pattern not detected
                case 100: { // Success(final result)
                    String digits = parameters.get("dc");
                    if (digits == null && parameters.get("asrr") != null) {
                        handleAsrr(parameters, code);
                    } else {
                        final IvrEndpointResponse result = new IvrEndpointResponse(
                                new CollectedResult(digits == null ? EMPTY_STRING : digits, AsrSignal.REQUEST_ASR.getName().equals(event.getName()), false));
                        for (final ActorRef observer : observers) {
                            observer.tell(result, self);
                        }
                    }
                    break;
                }
                case 101: { // Success(partial result)
                    handleAsrr(parameters, code);
                    break;
                }
                default: {
                    fail(code);
                }
            }
        }
    }

    @Override
    public void postStop () {
        if (logger.isInfoEnabled()) {
            logger.info("At IVR postStop()");
        }
        super.postStop();
    }
}
