import { FontAwesome } from '@expo/vector-icons';
import * as Linking from "expo-linking";
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, AppState, FlatList, Image, ImageURISource, KeyboardAvoidingView, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
import { Pulse, Wave } from 'react-native-animated-spinkit';
import Config from 'react-native-config';
import Modal from "react-native-modal";
import { SafeAreaView } from 'react-native-safe-area-context';
import Spokestack, { PipelineProfile } from 'react-native-spokestack';
import { Landmark, useLandmarks } from '../../../hooks/useLandmarks';
import { AuthTabsNavigationProp } from '../../../navigation/AuthorizedNavigator';
import { colors, GlobalStyles, lmTypes } from '../../../utils/GlobalUtils';
import { Separator } from '../../Separator';
import * as Speech from 'expo-speech';

export interface VoicePanelProps {
    voiceVisible: boolean,
    toggleVoiceVisible: (state: boolean) => void,
    toggleAlertedLandmarksVisible: (state: boolean) => void
    newLandmark: Landmark,
    setNewLandmark: (state: Landmark) => void,
    setSelectedLandmarkId: (state: string) => void,
    toggleLmDetails: (state: boolean) => void,
    userCoords: {latitude: number, longitude: number}
    navigation: AuthTabsNavigationProp
}

export interface VoiceAction {
    actionType: "Add landmark" | "Filter landmark"
    actionStep: number
}

/**
 * Panel that provides UI flows and backend logic for voice activated features. Relies on [Spokestack](https://www.npmjs.com/package/react-native-spokestack).
 * Check the documentation for more information about the Spokestack.initalize(), Spokestack.start(), and Spokestack.activate() and deactivate() methods.
 * A quick summary is as follows:
 * - Spokestack.initialize() -> Prepares the Spokestack service with a given configuration
 * - Spokestack.start() -> Starts the Spokestack service with the configuration given in initialize(). 
 *   After this method, Spokestack is ready to start recognizing speech, and can now be triggered by wakewords
 * - Spokestack.activate() -> Causes Spokestack to start listening and processing all speech and output the result. 
 *   This can be triggered manually or triggered by a wakeword 
 * - Spokestack.deactivate() -> Causes Spokestack to stop listening and processing all speech
 * - Spokestack.stop() -> After this method triggers, Spokestack no longer is able to listen for wakewords, or be activated
 * - Spokestack.destroy() -> The Spokestack service is torn down, and must be initialized with initialize() again for Spokestack to be used
 * @param 
 * @returns 
 */
export const VoicePanel: React.FC<VoicePanelProps> = ({
        navigation,
        toggleVoiceVisible, 
        toggleAlertedLandmarksVisible,
        voiceVisible,
        newLandmark, 
        userCoords,
        setNewLandmark,
        setSelectedLandmarkId}
    ) => {

    // An array of words that will cause the app to stop listening once heard
    const stopResponses = ["cancel", "stop", "goodbye", "bye"]

    // A flag that toggles whether the app is current listening
    const [listening, setListening] = useState<boolean>(false);

    // Holds the value of the intermediate speech results, which update as the user speaks
    const [partialSpeechResult, setPartialSpeechResult] = useState<string>();

    // Holds the value of the final speech results, updates once the user is determined to be finished speaking
    const [speechResult, setSpeechResult] = useState<string>();

    // Holds the value of the previously recognized result, and is displayed to the user for better UX
    const [previousResult, setPreviousResult] = useState<string>();

    // A string that represents a response that app gives to the user once it executes some voice recognition
    const [response, setResponse] = useState<string>();

    // Holds the current action state of a ongoing voice recognition flow
    const [action, setAction] = useState<VoiceAction>();
    const [nearbyLandmarksTrigger, toggleNearbyLandmarksTrigger] = useState<boolean>(false)
    const {addLandmarkAsync, addLandmarkStatus, resetAddLm} = useLandmarks();

    const containsAddVerb = () => {
        return (speechResult.includes("add") || speechResult.includes('create') || speechResult.includes('make'))
    }

    const containsViewVerb = () => {
        return (
            speechResult.includes("view") || 
            speechResult.includes("look") || 
            speechResult.includes("show"))
    }

    const containsNearAdjective = () => {
        return (
            speechResult.includes("near") || 
            speechResult.includes("close"))
    }

    const maybeOpenNearbyLandmarks = () => {
        if (nearbyLandmarksTrigger) {
            toggleNearbyLandmarksTrigger(false)
            toggleAlertedLandmarksVisible(true)
        }
    }

    useEffect(() => {
        console.log('Voice panel visible: ' + voiceVisible)
    }, [voiceVisible])

    useEffect(() => {
        if (nearbyLandmarksTrigger) {
            toggleVoiceVisible(false)
        }
    }, [nearbyLandmarksTrigger])

    // Sets up and starts the Spokestack process the first time this component is rendered
    useEffect(() => {
        // Adds handler that will open the app, navigate to the Map screen and open the panel if necessary when Spokestack.activate() is called.
        // Also, listening will be set to true, and speechResult will be reset
        Spokestack.addEventListener('activate', () => {
            console.log('Spokestack activated')
            if (!navigation.isFocused()) {
                navigation.navigate('Map')
            }
            if (AppState.currentState != 'active') {
                Linking.openURL("cnp.mobile://")
            }
            if (!voiceVisible) {
                toggleVoiceVisible(true)
            }
            setSpeechResult('')
            setListening(true)
        });
        // Adds handler that will set listening to false when Spokestack.deactivate() is called
        Spokestack.addEventListener('deactivate', () => {
            console.log('Spokestack deactivated')
            setListening(false)
        })
        // Adds handler that will be called when Spokestack finishes recognizing some speech (once the user stops speaking).
        // Sets speechResult and previousSpeechResult to the processed result 
        Spokestack.addEventListener('recognize', ({transcript}) => {
            console.log('Spokestack finished recognizing')
            setPartialSpeechResult('')
            setSpeechResult((transcript as string).trim().toLowerCase())
            setPreviousResult(transcript)
        })
        // Adds handler that will be called when Spokestack recognizes an update to the speech it is currently listening to.
        // Sets partialSpeechResult to the processed result
        Spokestack.addEventListener('partial_recognize', ({transcript}) => {
            console.log('Spokestack recognizing')
            setPartialSpeechResult(transcript)
        })  

        Spokestack.initialize(
            Config.SPOKESTACK_ID, // account id
            Config.SPOKESTACK_TOKEN, // api token
            {
                pipeline: {
                    profile: PipelineProfile.TFLITE_WAKEWORD_NATIVE_ASR
                },
                wakeword: {
                    detect: 'https://s.spokestack.io/u/kHNUa/detect.tflite',
                    filter: 'https://s.spokestack.io/u/kHNUa/filter.tflite',
                    encode: 'https://s.spokestack.io/u/kHNUa/encode.tflite',
                },
            }
        ).then(Spokestack.start).catch(error => console.log(error))

        return () => {
            Spokestack.removeAllListeners();
        };
    }, []);

    useEffect(() => {
        let isMounted = true;
        /**
         * Handles speech result once recognition completes 
         */
        const handleSpeechtranscript = async () => {
            // make sure the component is mounted to avoid errors
            if (isMounted) {
                // if the a speech result has been detected, and Spokestack is not currently listening, process that result
                if (speechResult && !listening) {
                    await processSpeechTranscript();
                }
                // if there is no speech result has been detected, and Spokestack is not currently listening
                if (!speechResult && !listening) {
                    // if there is no current action, treat it as a timeout and close the voice panel
                    if (!action) {
                        await closeVoice(false);   
                    }
                    else {
                        // otherwise give the user another chance to speak
                        setListening(true)
                        await Spokestack.activate()
                    }
                }
            }
          }
          handleSpeechtranscript()
          return () => {isMounted = false}
    }, [listening])

    /**
     * processes the speech recognition transcript
     */
    const processSpeechTranscript = async () => {
        // stop voice if speech result is one of the stop cases - highest priority
        if (stopResponses.some(response => speechResult.includes(response)) || (speechResult.includes('no') && !action) || (speechResult.includes('back') && !action)) {
            await closeVoice(true);
        }
        // when an action hasn't been initialized yet - discern which action is being requested
        else if (!action) {
            await discernActionRequest()
        }
        // when an action has been initialized - perform the associated action flow
        else if (action) {
            await performActionFlow()
        }
        // handle unrecognized result
        else {
            await Spokestack.activate();
        }
    }

    /**
     * method that initates an appropiate action for the detected action request. it executes when there isn't an action set yet.
     * each initate<ActionName>Action() method will set the action, then setResponse to the user accordingly.
     * once this method has been called, all subsequent transcript will be piped into performActionFlow
     */
    const discernActionRequest = async () => {
        // this branch will initiate whichever action is detected
        if (containsAddVerb() && speechResult.includes("landmark")) {
            await initiateAddLandmarkAction()
        }
        else if (speechResult.includes('near')) {
            await initiateViewNearLandmarksAction()
            return
        }
        else if (speechResult.includes("filter landmarks")) {
            // example action
        }
        // other possible actions

        // handle unrecognized result
        else {
            await respond("Sorry, I don't recognize that command. Try again.")
        }
        await Spokestack.activate()
    }

    // action initiators

    const initiateAddLandmarkAction = async () => {
        setNewLandmark({longitude: userCoords.longitude, latitude: userCoords.latitude})
        setAction({actionType: "Add landmark", actionStep: 1})
        await respond("Could you describe the landmark?")
        await Spokestack.activate();
    }   

    const initiateViewNearLandmarksAction = async () => {
        setAction(undefined)
        toggleNearbyLandmarksTrigger(true)
        await closeVoice(false)
    }

    // end action initiators


    /**
     * method that performs an appropriate action flow for the currently set action
     */
     const performActionFlow = async () => {
        // add landmark flow
        if (action.actionType == "Add landmark") {
            await performAddLandmarkActionFlow()
        }    
        // unrecognized speech
        else {
            await respond("Sorry, I don't recognize that command. Try again.")
        }

        await Spokestack.activate();
    }

// action flows

    // addLandmark flow

     const performAddLandmarkActionFlow = async () => {
        switch (action.actionStep) {
            case 1:
                await addLandmarkStep1();
                break;
            case 2:
                await addLandmarkStep2();
                break;
            case 3:
                await addLandmarkStep3();
                break;
        }
    }

    const addLandmarkStep1 = async () => {
        if (speechResult == 'back') {
            setAction(undefined);    
        }
        else {
            setNewLandmark({...newLandmark, description: speechResult})
            setAction({...action, actionStep: action.actionStep + 1});
            await respond("What type of landmark is it?")
        }
    }

    const addLandmarkStep2 = async () => {
        // get labels of icons from Icons dictionary
        const typeLabels = Object.values(lmTypes).map(icon => icon.label);
        // check if response matches any labels
        const chosenType = typeLabels.filter(response => speechResult.toLowerCase().includes(response.toLowerCase()));
        if (speechResult == 'back') {
            setAction({...action, actionStep: action.actionStep - 1})
            await respond("Could you describe the landmark?")
        }
        else if (chosenType.length > 0) {
            // get corrected icon id by getting index of chosen icon type in labels array created above
            setNewLandmark({...newLandmark, landmark_type: typeLabels.indexOf(chosenType[0]) + 1, title: chosenType[0]})
            setAction({...action, actionStep: action.actionStep + 1});
            await respond("Are you sure you want to add this landmark?")
        }
        else {
            setAction({...action, actionStep: action.actionStep});
            await respond("Sorry. I don't recognize that type of landmark. Please try again.")
        }
    }

    const addLandmarkStep3 = async () => {
        if (speechResult == 'back') {
            setAction({...action, actionStep: action.actionStep - 1})
            await respond("What type of landmark is it?")
        }
        else if (speechResult.includes("yes")) {
            setAction(undefined);
            await addLandmarkAsync({landmarkValue: newLandmark});
            await respond("Great, I've added the landmark. Anything else?")
        }
        else if (speechResult.includes("no")) {
            setAction(undefined);
            await respond("Okay, I didn't add the landmark. Anything else?")
        }
        else {
            setAction({...action, actionStep: 3});
            await respond("Okay, I didn't add the landmark. Anything else?")
        }
    }
    useEffect(() => {
        /**
         * Resets the {@link addLandmarkAsync} mutation, and reports to the user on successful add. Reports error on failure.
         * Embedded in a useEffect that listens to the {@link addLandmarkStatus} value from the {@link useLandmarks} hook.
         * @memberOf AddLandmark
         */
        const finishAddAttempt = async () => {
            if (addLandmarkStatus == 'success') {
                resetAddLm();
                setSelectedLandmarkId(newLandmark.id)
                await Spokestack.activate();
            }
            else if (addLandmarkStatus == 'error') {
                await Spokestack.activate();
            }
        }
        finishAddAttempt();
    }, [addLandmarkStatus]);

    // end add landmark flow

// end action flows
    
    const closeVoice = async (sayGoodbye: boolean) => {
        setSpeechResult('')
        setPreviousResult('')
        setResponse('')
        setListening(false)
        setAction(undefined);
        toggleVoiceVisible(false)
        if (sayGoodbye) {
            await respond('Goodbye!')   
        }
        await Spokestack.deactivate();
    }

    const respond = async (responseSpeech: string) => {
        await Spokestack.deactivate()
        Speech.speak(responseSpeech)
        while (true) {
            if (!(await Speech.isSpeakingAsync())) {
                break;
            }
        }
        await Spokestack.activate()
    }

    const back = async () => {
        setAction({...action, actionStep: action.actionStep - 1})
    }

    const LmTypeDisplay: React.FC<{lmType: {image: ImageURISource, label:string}, style?: ViewStyle}> = ({lmType, style}) => {
        return (
            <View style={[{marginVertical: 5, flexDirection: 'row'}, style]}>  
                <Image style={{height: 25, width: 18}} source={lmType.image} />
                <Text style={{fontSize: 15, marginLeft: 10, textAlign: 'center', color: 'white'}}>{lmType.label}</Text>
            </View>
        )
    }

    const ActionOptions: React.FC<{currentAction?: VoiceAction}> = ({currentAction}) => {
        if (currentAction) {
            switch (currentAction.actionType) {
                case 'Add landmark':
                    switch (currentAction.actionStep) {
                        case 1: 
                            return <Text style={{fontSize: 20, margin: 20, color: 'white'}}>Describe the landmark</Text> 
                        case 2:
                            return (
                                <View>
                                    <Text style={{fontSize: 17, marginHorizontal: 20, marginBottom: 5, color: 'white'}}>Your description:</Text>
                                    <Text style={{fontSize: 15, marginHorizontal: 20, marginBottom: 10, color:'white'}}>{newLandmark.description}</Text> 
                                    <Text style={{fontSize: 17, margin: 20, color: 'white'}}>Choose from the available types: </Text> 
                                    <FlatList 
                                        style={{marginBottom: 20, marginHorizontal:20}}
                                        data={Object.keys(lmTypes).map(typeId => typeId)}
                                        numColumns={2}
                                        keyExtractor={item => item}
                                        renderItem={(item => {
                                            return (
                                                <LmTypeDisplay key={item.item} lmType={lmTypes[item.item]} style={{flexBasis: '50%'}} />
                                            )})} />
                                </View>
                            )
                        case 3:
                            return (
                                <View>
                                    <Text style={{fontSize: 20, margin: 20, marginTop: 10, color: 'white'}}>Your landmark: </Text> 
                                    <View style={{marginLeft: 40, marginBottom: 20}}>
                                        <Text style={{fontSize: 17, marginBottom: 10, color: 'white'}}>Description: </Text> 
                                        <Text style={{fontSize: 15, marginBottom: 20, marginLeft: 20, color: 'white'}}>{newLandmark.description}</Text> 
                                        <Text style={{fontSize: 17, marginRight: 10, marginBottom: 10, color: 'white'}}>Type: </Text>
                                        <LmTypeDisplay lmType={lmTypes[newLandmark.landmark_type]} style={{marginLeft: 20}} />
                                    </View>
                                    <Text style={{fontSize: 17, marginHorizontal: 20, marginBottom:10, color: 'white'}}>Are you sure you want to add this landmark?</Text> 
                                    <Text style={{fontSize: 15, marginHorizontal: 40, marginBottom: 10, color: 'white'}}>- Say "Yes" to confirm</Text>
                                    <Text style={{fontSize: 15, marginHorizontal: 40, marginBottom: 20, color: 'white'}}>- Say "No" to cancel</Text>
                                </View>
                            )
                        case 4:
                            return (
                                <View>
                                    <Text style={{fontSize: 20, margin: 20, color: 'white'}}>Done. Anything else?</Text> 
                                </View>
                            )
                    }
            }   
        }
        
        if (addLandmarkStatus == 'loading') {
            return <ActivityIndicator color='white' size="large"/>
        }
        else if (addLandmarkStatus == "error") {
            return <Text style={{fontSize: 20, margin: 20, color: 'white'}}>Something went wrong when trying to upload the landmark.</Text>
        }
    }

    const ActionProcessingIndicator: React.FC = () => {
        return (
            <View style={{margin: 20}}>
                <Text style={{fontSize: 20, margin: 20, color: 'white'}}>Adding landmark...</Text>
                <ActivityIndicator color="white" size="large" />
            </View>
        )
    }

    const SpeechActivity: React.FC = () => {
        return (
            <View style={{justifyContent: 'center', margin: 20, padding: 10, backgroundColor: 'white'}}>
                {previousResult ? <Text style={{fontSize: 15, marginHorizontal: 20, marginBottom: 10, color: "black", alignSelf: 'center'}}>Last result: {previousResult}</Text> : null }
                {partialSpeechResult ?                         
                <Wave color={colors.red} hidesWhenStopped={false} animating={true} style={{alignSelf: 'center', marginBottom: 10}} /> : 
                <Pulse color={colors.red} hidesWhenStopped={false} animating={true} style={{alignSelf: 'center', marginBottom: 10}} />}
                <Text style={{fontSize: 15, marginHorizontal: 20, color: "black", alignSelf: 'center'}}>{
                    speechResult ? speechResult :
                    partialSpeechResult ? partialSpeechResult : ""}
                </Text>
            </View>
        )
    }

    const VoicePanelHeader: React.FC = () => {
        return (
            <View style={[GlobalStyles.itemRowContainer, {width: '100%'}]} >
                <Text style={{fontSize: 20, margin: 20, color: 'white', flexBasis: "70%"}}>{action ? action.actionType : "Waiting for command..."}</Text>
                <TouchableOpacity style={{margin: 20}} onPress={async () => await closeVoice(true)}><FontAwesome size={20} color='white' name='times'/></TouchableOpacity>
            </View>
        )
    }

    return (
    <Modal
        onModalHide={maybeOpenNearbyLandmarks}
        swipeDirection='down'
        useNativeDriver={true}
        useNativeDriverForBackdrop={true}
        avoidKeyboard={true}
        onBackdropPress={async () => await closeVoice(true)}
        style={{justifyContent: "flex-end", height: '100%', margin: 0}}
        isVisible={voiceVisible} >
        <KeyboardAvoidingView>
            <SafeAreaView style={{backgroundColor: colors.red}}>
                {addLandmarkStatus == "loading" ?
                <ActionProcessingIndicator /> : 
                <View>
                    <VoicePanelHeader />
                    <Separator color="lightgray" style={{marginHorizontal: 20, marginBottom: 20, opacity: .5}} />
                    {action ? 
                    <ActionOptions currentAction={action} /> : 
                    <View style={{marginBottom: 20}}>
                        <Text style={{fontSize: 15, marginHorizontal: 40, marginBottom: 10, color: 'white'}}>- Say "Add landmark" to add a landmark</Text>
                        <Text style={{fontSize: 15, marginHorizontal: 40, marginBottom: 10, color: 'white'}}>- Say "Near" to view nearby landmarks</Text>
                    </View>}
                    {response? <Text style={{fontSize: 13, marginHorizontal: 40, marginBottom: 20, color: 'white'}}>{response}</Text> : null}
                    <SpeechActivity />
                    <Text style={{fontSize: 13, marginHorizontal: 40, marginBottom: 20, color: 'white'}}>Say "cancel", "bye", "goodbye", or "stop" to exit.</Text>
                    <Text style={{fontSize: 13, marginHorizontal: 40, marginBottom: 20, color: 'white'}}>Say "back" to go back a step.</Text>
                </View>}
            </SafeAreaView>
        </KeyboardAvoidingView>
    </Modal>
    )
}