//
//  Copyright © 2017 IPsoft. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "IPSAKDownloadMessage.h"
#import "IPSAKMessage.h"
#import "IPSAKTrafficMonitor.h"

NS_ASSUME_NONNULL_BEGIN

@class IPSAKAuthSystem;
@class IPSAKConfiguration;
@class IPSAKDomain;
@class IPSAKFormInputData;
@class IPSAKLoginOptions;
@class IPSAKUploadMessage;
@class IPSAKUser;
@class IPSAKAppConfig;
@protocol IPSAKChatSessionDelegate;
@protocol IPSAKChatConversationDelegate;

/**
 Constants indicating the type of reconnect option.
 */
typedef NS_ENUM(NSInteger, IPSAKReconnectOption) {
    /// There is still a valid session, reconnect is possible.
    IPSAKReconnectOptionCanReconnect,
    /// The session is not valid, reconnect is not possible.
    IPSAKReconnectOptionInvalidSession,
    /// Could not determine if it is possible to reconnect or not.
    IPSAKReconnectOptionUnknown
};

/**
The version of the Amelia protocol this version of the SDK supports. 
 */
extern NSString *const IPS_AMELIA_PROTOCOL_VERSION;

/**
 The `IPSAKChat` class provides the main interaction point with Amelia. Initiate a new instance with a configuration object, set the `sessionDelegate` and the `conversationDelegate` and then call `startNewConversation` to get started.
 
 ## Overview
 
 Setting up a connection to Amelia is divided into two parts: establishing a session and sending messages to and receiving messages from Amelia. Establishing a session consists of several steps: getting a session cookie, creating a user (either anonymously or by providing credentials) to get a CSRFToken, getting a list of available domains, and finally starting a new conversation. Events related to this is handled via the `sessionDelegate`. After a conversation has been started the events related to the conversation are handled via the `conversationDelegate`.
 
 ### Session
 
 Starting a new conversation requires several network requests and the possibility of having to manually handle providing credentials and select which domain to join. The configuration object, passed in when creating a new instance, controls how the session is established. It is possible to specify if the SDK should try to start an anonymous session or if a login with credentials is required. Another option is whether a domain should be joined automatically or if the client should manually select which domain to join.
 
 To establish a session, call `startNewConversation` and handle the interaction using the methods of the `sessionDelegate`. The class is designed to handle failed network requests by providing a retry mechanism. If, for example, fetching the list of available domains fails due to loss of network, the process can continue by calling `retry` when the network is restored.
 
 ### Conversation
 
 When a new conversation has started, the `sessionDelegate` gets sent `chatConversationStart:`. All messages related to the current conversation is sent to `conversationDelegate`. When a conversation is active, send a question to Amelia using `say:` or `ask:`. Form input is sent using `submitForm:message:` and integration messages are handles using `runAction:arguments:message:`. Uploading a file in response to a `chat:uploadRequested:` is done with `uploadFileType:data:filename`.

 ## Using an IPSAKChat instance
 
 1. Create a configuration object and pass it in to `chatWithConfiguration:` Set the `sessionDelegate` and the `conversationDelegate`.
 
 2. Start by calling `startNewConversation`.
 
 After starting the chat instance calls the following methods on its `sessionDelegate`:
 
 
 1. If logging in with credentials, the response to a successful `login:` is `chatLoginSuccess:` and if there is an error,  the `IPSAKChat` object calls `chat:loginFailWithError:`.
 
 2. The login part of the process completes with a call to 'chat:didInitUser:'.
 
 3. The next step is fetching the domains. If a domain must be selected, either by having set IPSAKDomainSelectionModeManual or if there are too many domains to select automatically, the `IPSAKChat` object calls `chat:domainSelectionRequired:` and the client must call `selectDomain:` to continue.
 
 4. The last part is creating a new conversation using the selected domain. If the conversation is properly created the chat calls `chatConversationStart:` and all events related to the conversation is communicated via the `conversationDelegate`. If an error occurs, the chat calls `chat:conversationFailWithError:`.
 
 In case of a lost network connection it is possible to continue the process by calling `retry` on the `IPSAKChat` object. 
 
 @note It is possible to call `retry` after receiving any type of error, but other than some type of network errors probaly needs to solved in another way. If a conversation can't be started due to having specified the wrong domain, it must be solved by calling `selectDomain:` using another domain.
 
 When a conversation is started the method calls on the `conversationDelegate` does not come in a predefined order as they depend on interaction. Here are short description of the different type of events that a client may need to handle:
 
 - Receiving messages from Amelia. Most messages are received as a response to something the client did send and are communicted by the `IPSAKChat` object calling `chat:didReceiveMessage:`. Some messages can come at any time, but doesn't normally need to be handled by the client, such as `IPSAKMessageTypeOutboundAckRequest`. If Amelia sends a file, such as an image or pdf, or a link, the chat calls `chat:didReceiveDownloadMessage:` which includes an instance of an `IPSAKDownloadMessage` object. The client needs to call `download:` on the `IPSAKDownloadMessage` object to get the file data.
 
 - Amelia can request a file upload and then the chat calls `chat:uploadRequested:`. To continue the client should call `uploadFileType:data:filename:`. The result of the upload is reported by a call to `chatUploadSuccess:` or `chat:uploadFailWithError:` by the chat instance.
 
 - Amelia can also send structured data as a form, `formInputData` is non-il or as an integration message, `messageType == IPSAKMessageTypeOutboundIntegration`. To respond to the form the client calls `submitForm:message:`. To repsond to the integration message the client calls `runAction:arguments:message:`.
 
 - While Amelia is processing, the client is not allowed to send new data. When the input status changes, the `IPSAKChat` object calls `chat:inputEnabled:`. The client can also query the current input status using the `inputEnabled` property.
 
 - Amelia can express different moods. When a mood change occurs, the chat instance calls `chat:moodChangeFrom:to:`. The client can also query the current mood using the `currentMood` property.
 
 - If there is an error during a conversation, the chat instance calls `chat:error:`.
 
 ## Speech
 
 If `configuration.speechParams.enabled == YES` and the instance is umuted, messages received from Amelia will automatically be spoken. The text to speak is downloaded from Amelia and `IPSAKChat` uses `AVAudioPlayer` to play the sound. `IPSAKChat` will call `chat:willStartSpekaing:` on the conversation delegate before the sound plays. This lets the delegate configure the `AudioSession` to handle how the sound should be played, e.g. should the speech be mixed in with audio that is currently playing.
 
 
 This is an example of how to configure the speech to respect the silence switch and lower the volume of other audio playing while speaking the text.
 
 ```
 - (void)chatWillStartSpeak:(IPSAKChat *)chat {
    AVAudioSession *session = [AVAudioSession sharedInstance];
    NSError *activeError;
    if (![session setActive:YES error:&activeError]) {
        // Handle error
        return;
    }
 
    NSError *categoryError;
    if (![session setCategory:AVAudioSessionCategoryAmbient withOptions:AVAudioSessionCategoryOptionDuckOthers error:&categoryError]) {
        // Handle error
        return;
    }
 }
 
 - (void)chatDidEndSpeak:(IPSAKChat *)chat {
    [[AVAudioSession sharedInstance] setActive:NO error:nil];
 }
 ```
 */
@interface IPSAKChat : NSObject

/**
 Creates a chat with the specified configuration.
 
 @param configuration A configuration object that specifies certain behaviors, such as if anonymous login is allowed and how to select which domain to join. The configuration also contains the URL for the Amelia instance.
 
 @return An `IPSAKChat` instance.
 */
+ (instancetype)chatWithConfiguration:(IPSAKConfiguration *)configuration;

/// The object that acts as the session delegate of the chat.
@property (nonatomic, weak) id<IPSAKChatSessionDelegate> sessionDelegate;

/// The object that acts as the chat delegate of the chat.
@property (nonatomic, weak) id<IPSAKChatConversationDelegate> conversationDelegate;

/// The list of available domains
@property (nullable, nonatomic, strong, readonly) NSArray<IPSAKDomain *> *domains;

/// The current selected domain.
@property (nullable, strong, nonatomic, readonly) IPSAKDomain *domain;

/// The currently logged on user.
@property (nullable, nonatomic, strong, readonly) IPSAKUser *user;

/// The id of the conversation.
@property (nonatomic, readonly) NSString* conversationId;

/// The id of the session.
@property (nonatomic, readonly) NSString* sessionId;

/// Whether Amelia accepts input or not.
@property (nonatomic, readonly, getter=isInputEnabled) BOOL inputEnabled;

/// The timestamp of the last sent or received message.
@property (nullable, nonatomic, strong, readonly) NSDate *timeOfLastMessage;

/// Amelia's current mood.
@property (nonatomic, readonly) IPSAKMoodType currentMood;

/// The name of `Amelia`.
@property (nullable, nonatomic, copy, readonly) NSString *conversationPartnerName;

/**
 Whether the SDK should try to reconnect to a conversation when the conversation is unexpectedly disconnected.
 
 @note The current implementation just tries to reconnect every 2 seconds. Should be changed to use the reachability API.
 */
@property (nonatomic) BOOL autoReconnect;

#pragma mark - Session
/**
 initialize sdk so that an appConfig will be returned, which contains server info such as timeouts, anonymous user information, etc..
 */
-(void)initialize;
/**
 Initiate the process of starting a new conversation. Starting a conversation is a multistep process that requires several
 network requests and the implementer of the `sessionDelegate` may need to respond to events to complete the process.
 */
- (void)startNewConversation;
/**
 start new conversation with initialAttribute pairs and intialBpnVariable pairs. Either of the them can be nil.
 */
-(void)startNewConversationWithInitialAttributes:(NSDictionary *)initialAttributes andInitialBpnVariables:(NSDictionary*) initialBpnVariables;

/**
 start a new covnersaton using existing session.
 Must be in a valid session.
 */
-(void)resetConversation;

/**
 Cancel an ongoing process of establishing a session.
 
 @note Have no effect if a conversation is started.
 */
- (void)cancel;

/**
 Continue the current start conversation process, starting from the current state.
 
 @note It is possible to call `retry` after receiving any type of error, but other than some type of network errors probaly needs to solved in another way.
 */
- (void)retry;

/**
 Step up from an anonymous login to a credentialed login.
 */
- (void)stepUp;

/**
 Performs a login using the provided login options. Normally called in response to a `chat:loginRequired:`.
 
 @param options The options to use when logging in.
 */
- (void)login:(IPSAKLoginOptions *)options;

/**
 Selects which domain to use when starting a new conversation. Normally called in response to a `chat:domainSelectionRequired:`.
 */
- (void)selectDomain:(IPSAKDomain *)domain;

/**
 Logs out currently logged in user.
 */
- (void)logout;

/**
 Checks if the conversation can be reconnected
 
 @param sendSessionError The option to suppress any session error during the reconenct.
 */
- (void)reconnectConversation:(void(^)(IPSAKReconnectOption option))completionHandler andReportSessionError:(BOOL) sendSessionError;

/**
 Ends the current conversation.
 */
- (void)endConversation;

#pragma mark - Chat

/**
 Ask Amelia a question.
 @param question the message to send.
 */
- (void)ask:(NSString *)question;

/**
 Alias for `ask:`.
 @param statement the message to send.
 */
- (void)say:(NSString *)statement;

/**
 Send to Amelia custom conversation attributes during the chat
 */

-(void)sendCustomConversationAttributes:(NSDictionary*)attributes;
/**
 Submit a form to Amelia.
 
 @note This method should be uses to respond to a message that includes `formInputData`
 
 @param form The form included in the message.
 @param message The text to speak in the submission
 */
- (void)submitForm:(IPSAKFormInputData *)form message:(NSString *_Nullable)message;

/**
 Run the named BPN process on Amelia.
 
 @param processName The name of the BPN process to run.
 @param processArgs The arguments to pass to the process.
 @param message A custom message to send.
 */
- (void)runAction:(NSString *)processName arguments:(NSDictionary *)processArgs message:(NSString *)message;
/**
 cancel file upload, for example, when client sides does not support upload of certain file types
 */
- (void)cancleFileUpload;
/**
 Uploads the provided data to Amelia using `uploadFileData:filename:mimetype:` where mimetype is determined from the the passed in data.
 */
- (void)uploadFileType:(NSString *)fileType data:(NSData *)data filename:(NSString *)filename;

/**
 Uploads the provided data to Amelia.
 
 @note The result of the upload is reported on the `conversationDelegate` using `chatUploadSuccess:` or `chat:uploadFailWithError:`.
 
 @param fileType The requested file type. The file type is included when receiving the `chat:uploadRequested:` event.
 @param data The data to upload.
 @param filename The name of the file.
 @param mimetype The mimetype of the data.
 */
- (void)uploadFileType:(NSString *)fileType
                  data:(NSData *)data
              filename:(NSString *)filename
              mimetype:(NSString *)mimetype;
/**
 Tells the SDK to stop speaking the responses received from Amelia.
 */
- (void)mute;

/**
 Tells the SDK to speak the responses received from Amelia
 
 @note This only has an effect if speech is enabled. (Controlled via `speechParams` on the configuration object.
 */
- (void)unmute;

/**
 Tells the SDK to stop playing the audio and forget all the audios which haven't been played. Set isSpeaking to No.
 */
- (void)stopPlayAudio;

#pragma mark - Debug

/**
 A debug delegate that, if set, will be called with raw requests and responses sent to and recevied from the Amelia 
 instance.
 */
@property (nonatomic, weak) id<IPSAKTrafficMonitor> trafficMonitor;

@end

/**
 The `IPSAKChatSessionDelegate` protocol defines the optional methods you implement to make an `IPSAKChat` instance
 functional. Which methods to implement depends on the functionality of your application, but at a minimum the delegate
 should respond to `chatConversationStart:` and present a chat UI.
 */
@protocol IPSAKChatSessionDelegate<NSObject>

@optional

#pragma mark - Anonymous login
/**
 called back at the time time of intializing IPSAKChat oboject,
 contains server information including whether anonymous user is allowed and anonymous domains if any
 */
-(void)chatInitialized:(IPSAKChat*)chat withAppConfig:(IPSAKAppConfig *)appConfig;
/**
 Tells the delegate that an anonymous user has been created.
 */
- (void)chatSessionStart:(IPSAKChat *)chat;
/**
 Tells the delegate that a session could not be established, the anonymous login failed.
 */
- (void)chat:(IPSAKChat *)chat sessionFailWithError:(NSError *)error;

#pragma mark - Login

/**
 Tells the delegate that a login is required. To continue the connection process `login:` should be called.
 */
- (void)chat:(IPSAKChat *)chat loginRequiredWithAuthSystems:(NSArray<IPSAKAuthSystem *> *)authSystems;
/**
 Tells the delegate that a login succeeded.
 */
- (void)chatLoginSuccess:(IPSAKChat *)chat;
/**
 Tells the delegate that the session is now logged out.
 */
- (void)chatLogout:(IPSAKChat *)chat;
/**
 Tells the delegate the login failed.
 */
- (void)chat:(IPSAKChat *)chat loginFailWithError:(NSError *)error;

#pragma mark -

/**
 Tells the delegate that an anonymous login has completed successfully.
 */
- (void)chat:(IPSAKChat *)chat didInitUser:(IPSAKUser *)user;

#pragma mark - Domain

/**
 Tells the delegate that a domain must be selected. Call `selectDomain:` to continue the process.
 */
- (void)chat:(IPSAKChat *)chat domainSelectionRequired:(NSArray<IPSAKDomain *> *)domains;
/**
Tells the delegate that the language for the conversation has changed.
*/
- (void)chat:(IPSAKChat *)chat languageChangeFrom:(NSString *)prevLanguageCode to:(NSString *)languageCode;
/**
 Tells the delegate that fethcing the list of domains failed.
 */
- (void)chat:(IPSAKChat *)chat domainFailWithError:(NSError *)error;

#pragma mark - Conversation

/**
 Tells the delegate the a conversation has been started. Events regarding the chat are received on the `conversationDelegate`.
 */
- (void)chatConversationStart:(IPSAKChat *)chat;
/**
 Tells the delegate that a conversation failed to start.
 */
- (void)chat:(IPSAKChat *)chat conversationFailWithError:(NSError *)error;

@end

/**
 The `IPSAKChatConversationDelegate` protocol defines the optional methods you implement to make an `IPSAKChat` instance functional regarding the chat functionality.
 */
@protocol IPSAKChatConversationDelegate<NSObject>
@optional
/**
 Tells the delegate that a message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that an echo message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveEchoMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a final text message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveFinalTextMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that Amelia is ready to take user input.
 */
- (void)chat:(IPSAKChat *)chat didReceiveAmeliaReadyMessage: (IPSAKMessage *)message;
/**
 Tells the delegate that a final error message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveFinalErrorMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a progress text message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveProgressTextMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a avatar change voice message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveAvatarChangeVoiceMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a escalation started message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveEscalationStartedMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that an ack request message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveAckRequestMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a session closed message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveSessionClosedMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a conversation closed message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveConversationClosedMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that an idle talk message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveIdleTalkMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that an idle no talk message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveNoIdleTalkMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that an integration message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveIntegrationMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a form input message has been received
 */
-(void)chat:(IPSAKChat *)chat didReceiveFormInputMessage:(IPSAKMessage *)message;
/**
 tells the delegate that a bpn execution event message has been receivede
 */
-(void)chat:(IPSAKChat *)chat didReceiveBpnExecutionEventMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that an agent session changed message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveAgentSessionChangedMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a final replay message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveReplayFinishedMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a wolfram alpha final message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveWolframAlphaFinalMessage:(IPSAKMessage *)message;
/**
 Tells the delegate that a download message has been received from Amelia.
 */
- (void)chat:(IPSAKChat *)chat didReceiveDownloadMessage:(IPSAKDownloadMessage *)message;
/**
 Tells the delegate that an upload is requested.
 */
- (void)chat:(IPSAKChat *)chat uploadRequested:(IPSAKUploadMessage *)uploadRequest;
/**
 Tells the delegate that an upload has successfully completed.
 */
- (void)chatUploadSuccess:(IPSAKChat *)chat;
/**
 Tells the delegate that an upload failed.
 */
- (void)chat:(IPSAKChat *)chat uploadFailWithError:(NSError *)error;
/**
 Tells the delegate whether Amelia is ready to receive input or not.
 */
- (void)chat:(IPSAKChat *)chat inputEnabled:(BOOL)enabled;
/**
 Tells the delegate whether user's input should be secured.
 */
- (void)chat:(IPSAKChat *)chat inputSecured:(BOOL)enabled;

/**
 Tells the delegate that input is currently blocked.
 */
- (void)chatErrorInputBlocked:(IPSAKChat *)chat;
/**
 Tells the delegate that the conversation is now idle.
 */
- (void)chatIdle:(IPSAKChat *)chat;
/**
 Tells the delegate the SDK will start speaking.
 */
- (void)chatWillStartSpeaking:(IPSAKChat *)chat;
/**
 Tells the delegate the SDK has stopped speaking.
 */
- (void)chatDidStopSpeaking:(IPSAKChat *)chat;
/**
 Tells the delegate that Amelia's mood has changed.
 */
- (void)chat:(IPSAKChat *)chat moodChangeFrom:(IPSAKMoodType)fromMood to:(IPSAKMoodType)toMood;
/**
 Tells the delegate that the chat session has ended when the server initiated the ending.
 */
- (void)chatDidEnd:(IPSAKChat *)chat;
/**
 Tells the delegate that then conversation is disconnected unexpectedly.
 */
- (void)chatDidDisconnect:(IPSAKChat *)chat;
/**
 Tells the delegate that the conversation is reconnected after a previous disconnect.
 */
- (void)chatDidRecconnect:(IPSAKChat *)chat;
/**
 Tells the delegate that an error occurred while chatting.
 */
- (void)chat:(IPSAKChat *)chat error:(NSError *)error;
@end

NS_ASSUME_NONNULL_END
