#!/usr/bin/env python

"""
Copyright 2017 Parham Pourdavood

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.
"""

from __future__ import division
from SimpleAudioIndexer import SimpleAudioIndexer as sai
from subprocess import Popen as popen
from watson_developer_cloud import ToneAnalyzerV3
from math import floor
import ast
import argparse
import speech_recognition as sr
import os
import httplib
import urllib
import ConfigParser
import shutil
import matplotlib.pyplot as plt
import numpy as np


def video_to_audio(video_abs_path, audio_abs_path):
    """
    Converts a video file to audio

    Paramteters
    -----------
    video_abs_path     str
    audio_abs_path     str
    """

    popen("ffmpeg -i {} -vn -acodec pcm_s16le -ar 44100 -ac 2 {}".format(
        video_abs_path, audio_abs_path), shell=True).communicate()


def audio_to_text(audio_abs_path, IBM_USERNAME, IBM_PASSWORD):
    """
    Converts audio voice into written text using IBM Watson

    Parameters
    ----------
    audio_abs_path     str
    IBM_USERNAME       str
    IBM_PASSWORD       str

    Returns
    -------
    data          str
    """
    r = sr.Recognizer()

    with sr.AudioFile(audio_abs_path) as f:
        audio = r.record(f)

    data = r.recognize_ibm(audio, username=IBM_USERNAME, password=IBM_PASSWORD)
    return data


def tone_json_maker(text_input, TONE_USERNAME, TONE_PASSWORD):
    """
    Takes a text as parameter and returns JSON created by IBM Tone Analyzer API

    Parameters
    ----------
    text_input      str
    TONE_USERNAME   str
    TONE_PASSWORD   str

    Returns
    -------
    jsonOutput      dict
    """
    tone_analyzer = ToneAnalyzerV3(
        username=TONE_USERNAME,
        password=TONE_PASSWORD,
        version='2016-05-19')

    jsonOutput = tone_analyzer.tone(text=text_input)

    return jsonOutput


def sentence_tone_maker(tones_list):
    """
    It takes a list which is the value of key of "tones" that is created for
    each sentence in the JSON created by tone_json_make above. The function
    returns the tone that best describes the sentence.

    Paramters
    ---------
    tones_list      list

    Returns
    -------
    best_tone       str
    """
    highest_score = 0
    best_tone = ""

    for i in range(4):
        if tones_list[i]["score"] > highest_score:
            highest_score = tones_list[i]["score"]
            best_tone = tones_list[i]["tone_name"]

    return best_tone


def sentence_tone_model(json_input):
    """
    Takes the JSON created by tone_json_maker and returns a model that we will
    use later in form of a dictionary.The model is a dictionary with keys being
    emotions and the value for each emotion is a list that consists of dicts
    with keys being sentences and values being empty tuples that we will use
    later.

    Paramters
    ---------
    json_input  dict

    Returns
    -------
    dict        dict

    """
    dict = {"anger": [], "disgust": [], "fear": [], "joy": [], "sadness": []}

    sentences_number = len(json_input["sentences_tone"])

    for i in range(sentences_number):
        sentences_text = json_input["sentences_tone"][i]["text"]

        sentences_list = json_input["sentences_tone"][i]["tone_categories"][0]["tones"]

        if sentence_tone_maker(sentences_list) == "Anger":
            dict["anger"].append({sentences_text[:-1]: ()})

        elif sentence_tone_maker(sentences_list) == "Disgust":
            dict["disgust"].append({sentences_text[:-1]: ()})

        elif sentence_tone_maker(sentences_list) == "Fear":
            dict["fear"].append({sentences_text[:-1]: ()})

        elif sentence_tone_maker(sentences_list) == "Joy":
            dict["joy"].append({sentences_text[:-1]: ()})

        else:
            dict["sadness"].append({sentences_text[:-1]: ()})

    return dict


class audio_analyzer(object):

    def __init__(self, username, password, src_dir):
        self.username = username
        self.password = password
        self.src_dir = src_dir
        self.indexer = sai(username, password, src_dir)
        self.indexer.index_audio()

    def audio_search(self, query, audio_basename=None,
                     part_of_bigger_word=False, timing_error=0.1):
        """
        A generator that searches for the `query` within the audiofiles of the
        src_dir.
        Parameters
        ----------
        query          str
                        A string that'll be searched. It'll be splitted on
                        spaces and then each word gets sequentially searched
        audio_basename str
                        Search only within the given audio_basename
        part_of_bigger_word     bool
                                `True` if it's not needed for the exact word be
                                detected and larger strings that contain the
                                given one are fine. Default is False.
        timing_error    float
                        Sometimes other words (almost always very small) would
                        be detected between the words of the `query`. This
                        parameter defines the timing difference/tolerance of
                        the search. By default it's 0.1, which means it'd be
                        acceptable if the next word of the `query` is found
                        before 0.1 seconds of the end of the previous word.
        Yields
        ------
        -               {"File Name": str,
                         "Query": `query`,
                         "Result": (float, float)}
                         The result of the search is returned as a tuple which
                         is the value of the "Result" key. The first element
                         of the tuple is the starting second of `query` and
                         the last element is the ending second of `query`
        """
        return self.indexer.search(query, audio_basename=None,
                                   part_of_bigger_word=False,
                                   timing_error=0.5)


def time_stamps_adder(model_input, username, password, src_dir):
    """
    This function uses audio_search method of audio_analyzer class to index the
    sentences in the audio and find the time stamps they happen. It then adds
    the time stamps for each sentence in the unfinished model that was returned
    by sentence_tone_model function.

    Parameters
    ----------
    model_input:        dict
                        Returned by sentence_tone_model function
    username:           str
    password:           str
    src_dr:             str
                        The directory that the audio converted from orig. video
    Returns
    -------
    model_input:        dict
    """
    searcher = audio_analyzer(username, password, src_dir)

    for i in model_input.keys():

        if model_input[i] != []:

            for j in model_input[i]:

                tmp = ' '.join(filter((lambda x: "'" not in x),
                               j.keys()[0].split(" ")))

                search_result = (list(searcher.audio_search(tmp)))

                try:
                    stamp_tuple = search_result[0]['Result']

                    j[j.keys()[0]] = stamp_tuple

                    if stamp_tuple == ():

                        model_input[i].remove(j)

                except IndexError:

                    model_input[i].remove(j)
                    continue

    return model_input


def picture_emotion(image_src, API_KEY):
    """
    This function detects the face in a picture and returns the most probable
    emotion that is recognized in the face.

    Parameters
    ----------
    image_src:      str
    API_KEY:        str

    Returns
    -------
    emotion_name:   str
    """

    headers = {
        # Request headers
        'Content-Type': 'application/octet-stream',
        'Ocp-Apim-Subscription-Key': API_KEY,
    }

    params = urllib.urlencode({
        # Request parameters
        'outputStyle': 'perFrame',
    })
    with open(image_src, 'rb') as f:
        data = f.read()

    try:
        conn = httplib.HTTPSConnection('westus.api.cognitive.microsoft.com')

        conn.request("POST", "/emotion/v1.0/recognize?%s" % params, data,
                     headers)
        response = conn.getresponse()

        data = ast.literal_eval(response.read())

        conn.close()

        emotion_dict = data[0]["scores"]

        highest_score = 0

        emotion_name = ""

        for i in emotion_dict.keys():

            if emotion_dict[i] > highest_score:

                highest_score = emotion_dict[i]

                emotion_name = i

        if emotion_name == "happiness":

            emotion_name = "joy"

        return emotion_name

    except Exception:
        return "Not recognized"


def seconds_formatter(seconds):
    """
    Converts total seconds to HH:MM:SS.tttt format

    Parameters
    ----------
    seconds            str or numeric

    Returns
    -------
    -                  str
    """
    if type(seconds) == str:
        seconds = float(seconds)

    minutes, seconds = divmod(floor(seconds), 60)

    hours, minutes = divmod(minutes, 60)

    if minutes < 10:
        minutes = "0" + str(int(minutes))
    else:
        minutes = int(minutes)
    if hours < 10:
        hours = "0" + str(int(hours))
    else:
        hours = int(hours)
    if seconds < 10:
        seconds = "0" + str(int(seconds))
    else:
        seconds = int(seconds)

    formatted = "{}:{}:{}".format(hours, minutes, seconds)

    return formatted


def get_frame_emotion(video_src, image_dest, frame_time):
    """
    This function takes a snapshot of a video file given a certain time.certain

    Parameters
    ----------
    video_src:      str
    image_dest:     str
    frame_time:     str
                    It must be in form of HH:MM:SS (see seconds_formatter func)
    """
    popen("ffmpeg -y -ss {} -i {} -vframes 1 -q:v 2 {}".format(
        frame_time, video_src, image_dest), shell=True).communicate()


def face_emotion_adder(model_input, video_src, image_dest, API_KEY):
    """
    Goes over the model and according the timestamp of each sentence uses the
    get_frame_emotion to take a snapshot of the video at the middle of the time
    that the sentence is happened. It then uses picture_emotion function to
    give the emotion of the face in that picture. Face emotion are added into
    the model at the end. This is out final model.

    Parameters
    ----------
    model_input:        dict
    video_src:          str
    image_Dest:         str
    API_KEY:            str
                        Microsoft Emotion API Key
    Returns
    -------
    model_input:        dict
                        New and final model with face emotions added
    """
    for i in model_input.keys():

        if model_input[i] != []:

            for j in model_input[i]:

                if j[j.keys()[0]]:

                    time_average = (j[j.keys()[0]][0] + j[j.keys()[0]][1]) / 2

                    time = seconds_formatter(time_average)

                    get_frame_emotion(video_src, image_dest, time)

                    a = picture_emotion(image_dest, API_KEY)

                    j[j.keys()[0]] = a

    return model_input


def final_analysis(model_input):
    """
    "Change the format of the model so that the keys are now sentenced with
    keys beng their face and speech content emotions."

    Parameter
    ---------
    model_input:        dict

    Returns
    -------
    final_dict:         dict
    """
    final_dict = {}

    for i in model_input.keys():

        if model_input[i] != []:

            for j in model_input[i]:

                final_dict[j.keys()[0]] = {"text": i, "face": j[j.keys()[0]]}

    return final_dict


def get_analysis(model_input):
    """
    Returns a model that consists of frequency of data of our final model.

    Parameters
    ----------
    model_input:        dict
                        Result of final_analysis function

    Returns
    -------
    analysis_model:     dict

    """
    analysis_model = {"total": 0, "matched": 0, "unmatched": 0,
                      "joy": [0, 0], "anger": [0, 0], "fear": [0, 0],
                      "disgust": [0, 0], "sadness": [0, 0]}

    for i in final_an.keys():
        number_of_sentences = len(final_an.keys())

        analysis_model["total"] = number_of_sentences

        if final_an[i]["text"] == final_an[i]["face"]:
            analysis_model["matched"] += 1
            analysis_model[final_an[i]["text"]][1] += 1
        else:
            analysis_model["unmatched"] += 1

        analysis_model[final_an[i]["text"]][0] += 1

    return analysis_model


def generator(src_vid, IBM_USERNAME, IBM_PASSWORD, TONE_USERNAME,
              TONE_PASSWORD, Microsfot_API_KEY):
    """
    Uses all the functions created in this file to create the final model.
    To recap: Video to audio, speech to text, get the tone of each sentence,
    use each sentence to index the audio and get the timestaps for them,
    add the timestamps to the model, use the time stamps to take snapshot of
    that moment the sentence is spoken, detect the emotion in the face,
    add the face emotion to the model. Done!

    Parameters
    ----------
    src_vid:            str
    IBM_USERNAME:       str
    IBM_PASSWORD:       str
    TONE_USERNAME:      str
    TONE_PASSWORD:      str
    Microsfot_API_KEY:  str

    Returns
    -------
    complete_model:     dict

    """
    video_dir = os.path.dirname(src_vid)

    ata_folder = os.path.join(video_dir, ".ata")

    # Deleter the unecessary file if it was created for a previous processing
    # in order to process the video
    if os.path.exists(ata_folder):
        shutil.rmtree(ata_folder)

    os.mkdir(ata_folder)

    src_audio = os.path.join(ata_folder, "audio.wav")

    src_image = os.path.join(ata_folder, "image.jpg")

    video_to_audio(src_vid, src_audio)

    ata_text = audio_to_text(src_audio, IBM_USERNAME, IBM_PASSWORD)

    tone_json = tone_json_maker(ata_text, TONE_USERNAME, TONE_PASSWORD)

    tone_model = sentence_tone_model(tone_json)

    timed_model = time_stamps_adder(tone_model, IBM_USERNAME,
                                    IBM_PASSWORD, ata_folder)
    complete_model = face_emotion_adder(timed_model, src_vid, src_image,
                                        Microsfot_API_KEY)

    shutil.rmtree(ata_folder)

    return complete_model


def pi_char_generator(sizes, label_category, dest, name, title):
    """
    Function for generating a custom pie chart using matplotlib.

    Paramters
    ---------
    sizes:                  list
                            A list that contains the sizes for the chart
                            elements

    label_category:         str
                            Either matchness or emotions_total. Determines
                            which kind of chart use for template.label_category

    dest:                   str
                            Destination of the chart

    name:                   str
                            name of the file with its extension (png, pdf, ...)

    titleL                  str
                            Title of the chart that goes on the top

    Returns
    -------
    -                       Creates a pie chart file

    """
    if label_category == "matchness":
        labels = 'Matched', 'Unmatched'
        explode = (0, 0.2)

    elif label_category == "emotions_total":
        labels = 'joy', 'anger', 'fear', 'disgust', 'sadness'
        explode = (0, 0, 0, 0, 0)

    fig1, ax1 = plt.subplots()

    ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
            shadow=True, startangle=90)

    ax1.axis('equal')

    plt.title(title, y=-0.1)

    dest = os.path.join(dest, name)

    plt.savefig(dest)


def bar_char_generator(numbers1, numbers2, dest, name):
    """
    Function for generating a custom bar chart using matplotlib.

    Parameters
    ----------
    number1:                list
                            A list containing frequencies for total sentences
                            for each emotion

    number2:                list
                            A list containing frequencies for matched sentences
                            for each emotion

    dest:                   str
                            Destination of the chart

    name:                   str
                            name of the file with its extension (png, pdf, ...)

    Returns
    -------
    -                       Creats a bar chart file

    """

    n_groups = 5

    means_frank = numbers1

    means_guido = numbers2

    # create plot
    fig, ax = plt.subplots()

    index = np.arange(n_groups)

    bar_width = 0.35

    opacity = 0.8

    rects1 = plt.bar(index, means_frank, bar_width,
                     alpha=opacity,
                     color='b',
                     label='Total')

    rects2 = plt.bar(index + bar_width, means_guido, bar_width,
                     alpha=opacity,
                     color='g',
                     label='Matched')

    plt.xlabel('Emotions')

    plt.ylabel('Frequency')

    plt.title('Chart for comparing Total and Matched Emotions')

    plt.xticks(index + bar_width, ('Joy', 'Anger', 'Fear', 'Disgust', "Sadness"))

    plt.legend()

    dest = os.path.join(dest, name)

    plt.savefig(dest)


def total_emotion_calculator(analysis_model, category_number):
    """
    Given the analysis_model created by get_analysis function, returns a list
    containing percentages of frequencies of the sentences whose emotions were
    recognized in the text by Watson or all the sentences whose text tone
    matched its face emotion.

    Parameters
    ----------
    analysis_model:             dict
                                created by get_analysis function

    category_number:            int
                                Eitehr 0 or 1. 0 refers to all sentences and 1
                                refers to only sentences whose emotions were
                                matched

    Returns
    -------
    emotion_total_sizes:        list

    """
    joy_total = analysis_model["joy"][category_number]

    anger_total = analysis_model["anger"][category_number]

    fear_total = analysis_model["fear"][category_number]

    disgust_total = analysis_model["disgust"][category_number]

    total_sentences = analysis_model["total"]

    # Find the percentages

    joy_percent = joy_total / total_sentences

    anger_percent = anger_total / total_sentences

    fear_percent = fear_total / total_sentences

    disgust_percent = disgust_total / total_sentences

    # Find the proportionate size for each emotion

    joy_size = int(round(joy_percent, 2) * 100)

    anger_size = int(round(anger_percent, 2) * 100)

    fear_size = int(round(fear_percent, 2) * 100)

    disgust_size = int(round(disgust_percent, 2) * 100)

    sad_size = abs((joy_size + anger_size + fear_size + disgust_size) - 100)

    # Add the sizes to a list

    emotion_total_sizes = [joy_size, anger_size, fear_size, disgust_size,
                           sad_size]

    return emotion_total_sizes


def emotion_comparison_generator(analysis_model):
    """
    Given the analysis_model created by get_analysis function, returns a tuple
    containing two lists that are number of frequencies of all the sentences
    whose emotions were recognized in the text by Watson and all the sentences
    whose text tone matched its face emotion.

    Parameters
    ----------
    analysis_model:                         dict
                                            created by get_analysis function

    Returns
    -------
    (total_numbers, matched_numbers):       tuple
                                            Contains total_numebrs and
                                            matched numbers
    """

    # Find the toal sentences for each emotion

    joy_total = analysis_model["joy"][0]

    anger_total = analysis_model["anger"][0]

    fear_total = analysis_model["fear"][0]

    disgust_total = analysis_model["disgust"][0]

    sadness_total = analysis_model["sadness"][0]

    # Find the total matched sentences for each emotion

    joy_matched = analysis_model["joy"][1]

    anger_matched = analysis_model["anger"][1]

    fear_matched = analysis_model["fear"][1]

    disgust_matched = analysis_model["disgust"][1]

    sadness_matched = analysis_model["sadness"][1]

    # Add the total numbers to a lsit

    total_numbers = [joy_total, anger_total, fear_total, disgust_total,
                     sadness_total]

    # Add the matched numbers to a lsit

    matched_numbers = [joy_matched, anger_matched, fear_matched,
                       disgust_matched, sadness_matched]

    return(total_numbers, matched_numbers)


def graph_generator(analysis_model, dest):
    """
    Gets information from analysis_model created by get_analysis function to
    generate charts (pi_charts and bar charts).matched_numbers

    Parameters
    ----------
    analysis_model:         dict

    dest:                   str

    Returns
    -------
    -                       Creates chart files in dest folder

    """
    matched = analysis_model["matched"]

    unmatched = analysis_model["unmatched"]

    matched_percent = matched / unmatched

    matched_size = int(round(matched_percent, 2) * 100)

    unmatched_size = abs(matched_size - 100)

    matchness_sizes = [matched_size, unmatched_size]

    pi_char_generator(matchness_sizes, "matchness", dest, "matchness.png",
                      "Matched sentences vs Unmatched Sentences")

    emotion_total_sizes = total_emotion_calculator(analysis_model, 0)

    pi_char_generator(emotion_total_sizes, "emotions_total", dest,
                      "emotions_total.png",
                      "Ratio of all sentences based on speech's tone")

    emotion_matched_sizes = total_emotion_calculator(analysis_model, 1)

    pi_char_generator(emotion_matched_sizes, "emotions_total", dest,
                      "emotions_matched.png",
                      "Ratio of matched sentences based on speech's tone")

    numbers = emotion_comparison_generator(analysis_model)

    bar_char_generator(numbers[0], numbers[1],
                       dest, "general_data.png")


def argument_handler():
    """
    Argparse argument handler.

    """
    parser = argparse.ArgumentParser()

    group = parser.add_mutually_exclusive_group(required=True)

    group.add_argument("-credentials", "--credentials",
                       help="Command for saving API credentials", type=str)
    group.add_argument("-v", "--src_vid", help="Therapy's recording video",
                       type=str)
    parser.add_argument("-d", "--destination", help="result destination",
                        type=str)

    args = parser.parse_args()

    cred_path = os.path.join(os.path.expanduser("~"), ".ata_creds.ini")

    if args.src_vid:
        if not os.path.exists(cred_path):
            parser.error("The credentials file has not been created yet")
        if not args.destination:
            parser.error("Please enter a destination for generated result.")

    return (args.credentials, args.src_vid, cred_path, args.destination)


def ConfigParser_handler(credentials):
    """
    It uses Configparser to add our API credentials saved for the user in the
    their machine. A INI file called ".ata_credentials.ini" will be added in
    the home directory of user. The parameter credentials should be a string
    with API usernames and paswords seperated with spaces and in this order:
    IBM_USERNAME, IBM_PASSWORD, TONE_USERNAME, TONE_PASSWORD, Microsfot_API_KEY

    Parameters
    ----------
    credentials:        str

    """
    cred_list = credentials.split(" ")

    config = ConfigParser.ConfigParser()

    config.add_section("Keys")
    config.set("Keys", "IBM_USERNAME", cred_list[0])
    config.set("Keys", "IBM_PASSWORD", cred_list[1])
    config.set("Keys", "TONE_USERNAME", cred_list[2])
    config.set("Keys", "TONE_PASSWORD", cred_list[3])
    config.set("Keys", "Microsfot_API_KEY", cred_list[4])

    config_path = os.path.join(os.path.expanduser("~"), ".ata_creds.ini")

    with open(config_path, "wb") as config_file:
        config.write(config_file)

    if os.path.exists(config_path):
        print "Sucess! Your credentials were saved."
    else:
        print "Failed. Your credentials could not be saved."


def ConnfigParser_reader(cred_path):
    """
    This function gets the credential file's path that was made with
    Configparser and reads it for the user.

    Parameters
    ----------
    cred_path:      str

    Returns
    -------
    (IBM_USERNAME, IBM_PASSWORD, TONE_USERNAME,
     TONE_PASSWORD, Microsfot_API_KEY

    """
    config = ConfigParser.ConfigParser()

    config.read(cred_path)

    IBM_USERNAME = config.get("Keys", "IBM_USERNAME")

    IBM_PASSWORD = config.get("Keys", "IBM_PASSWORD")

    TONE_USERNAME = config.get("Keys", "TONE_USERNAME")

    TONE_PASSWORD = config.get("Keys", "TONE_PASSWORD")

    Microsfot_API_KEY = config.get("Keys", "Microsfot_API_KEY")

    return(IBM_USERNAME, IBM_PASSWORD, TONE_USERNAME,
           TONE_PASSWORD, Microsfot_API_KEY)


if __name__ == '__main__':
    credentials, src_vid, cred_path, destination = argument_handler()

    if credentials:
        ConfigParser_handler(credentials)

    else:

        # Return the result from argparse_handler and assign them

        IBM_USERNAME, IBM_PASSWORD, TONE_USERNAME, TONE_PASSWORD, Microsfot_API_KEY = ConnfigParser_reader(cred_path)

        # Generate the final model using generator function

        result = generator(src_vid, IBM_USERNAME, IBM_PASSWORD, TONE_USERNAME,
                           TONE_PASSWORD, Microsfot_API_KEY)

        # Change the model using and make it ready for analyzing

        final_an = final_analysis(result)

        final_result = get_analysis(final_an)


        # Set the path where we export the result to

        ata_path = os.path.join(destination, "ATA")

        # Create suffix for path folder in case one exists

        folder_suffix = 0

        # Increment the suffix while a foder with the same name exists

        while os.path.exists(ata_path + str(folder_suffix)):
            folder_suffix += 1

        ata_path = os.path.join(destination, "ATA{}".format(folder_suffix))

        # Finaly create the folder

        os.mkdir(ata_path)

        # Establish the path for the folder charts will be exported to

        chart_path = os.path.join(ata_path, "ata_charts")

        # Create the path for our charts

        os.mkdir(chart_path)

        # Using graph_generator function we create and export all the charts to
        # our designated path

        graph_generator(final_result, chart_path)

        # The path for our text model file

        result_path = os.path.join(ata_path, "ata_result.txt")

        # Create a text file to write our models to it

        with open(result_path, "w") as f:
            f.write(str(final_an) + "\n-----------------------------------\n" +
                    str(result))
