"""
Creates a multiprocess, multithread application to detect high
readings.

https://www.assemblesoftware.com/accelerometerexample
"""

import sys
import os
import math
import time

sys.path.append(os.path.abspath("../../IoTPy/multiprocessing"))
sys.path.append(os.path.abspath("../../IoTPy/core"))
sys.path.append(os.path.abspath("../../IoTPy/agent_types"))
sys.path.append(os.path.abspath("../../IoTPy/helper_functions"))

# multicore is in multiprocessing
from multicore import multicore, copy_data_to_stream
# stream is in core
from stream import Stream
# op, merge, source, sink are in agent_types
from op import map_element, map_window
from merge import zip_map, merge_window
from sink import stream_to_file

# constants related to reading data from file
TIME_INTERVAL = 0.0005
WINDOW_SIZE = 1500
# threshold for determining anomaly
ANOMALY_THRESHOLD = 0.01


# COMPUTE FUNCTION f FOR THE PROCESS THAT GENERATES LOCAL ANOMALIES
def f(in_streams, out_streams):
    """
    Parameters
    ----------
    in_streams: list of Stream
      in_streams is usually a list of 3 streams indicating
      measurements in x, y and z directions or equivalently
      in e, n, and z (for east, north, vertical) directions.
      Each triaxial sensor generates x, y and z streams.
    out_streams: list of Stream
      out_streams has only one element, which is a
      Stream of int. An element of this stream is either
      1.0 or 0.0. An element is 1.0 to indicate that an
      anomaly was detected and is 0.0 otherwise.
    """
    # DECLARE STREAMS

    # 1. zero_means
    # Array of stream with one stream for each stream in in_streams
    # zero_means[0, 1, 2] usually represent streams in the x, y, z direction generated by a single triaxial sensor.
    zero_means = [Stream('zero_means_' + str(i)) for i in range(len(in_streams))]

    # magnitudes is a stream of magnitudes of a vector from its x, y, z values
    magnitudes = Stream('magnitudes')

    # CREATE AGENTS

    # 1. subtract_mean agent
    # Define the terminating function
    def subtract_mean(window):
        return window[-1] - sum(window) / float(len(window))

    # Wrap the terminating function to create an agent
    for i in range(len(in_streams)):
        map_window(
            func=subtract_mean,
            in_stream=in_streams[i],
            out_stream=zero_means[i],
            window_size=500, step_size=1,
            initial_value=0.0
        )

    # 2. magnitude agent
    # Define the terminating function
    def magnitude_of_vector(coordinates):
        return math.sqrt(sum([v * v for v in coordinates]))

    # Wrap the terminating function to create an agent
    zip_map(
        func=magnitude_of_vector,
        in_streams=zero_means,
        out_stream=magnitudes
    )

    # 3. local anomaly agent
    # Define the terminating function
    def simple_anomaly(value):
        if value > ANOMALY_THRESHOLD:
            return 1.0
        else:
            return 0.0

    # Wrap the terminating function to create an agent
    map_element(
        func=simple_anomaly,
        in_stream=magnitudes,
        out_stream=out_streams[0]
    )


# THE COMPUTE FUNCTION g FOR THE AGGREGATION PROCESS
def g(in_streams, out_streams):
    """
    Parameters
    ----------
    in_streams: list of Stream
      in_streams is a list of anomaly streams with one stream from
      each sensor. An anomaly stream is a sequence of 0.0 and 1.0
      where 0.0 indicates no anomaly and 1.0 indicates an anomaly.
    out_streams: list of Stream
      This list consists of a single stream that contains 0.0
      when no global anomaly across all sensors is detected and 1.0
      when a global anomaly is detected.

    """

    # DECLARE STREAMS
    # Internal steam used in g
    regional_anomalies = Stream('Regional anomalies')

    # CREATE AGENTS

    # 1. aggregation agent
    # Define the terminating function
    def aggregate(windows):
        number_local_anomalies = [any(window) for window in windows].count(True)
        if number_local_anomalies > 1:
            return 1.0
        else:
            return 0.0

    # Wrap the terminating function to create an agent
    merge_window(
        func=aggregate,
        in_streams=in_streams, out_stream=regional_anomalies,
        window_size=250, step_size=1, initial_value=0.0
    )

    # 2. agent that copies stream to file
    for i in range(len(in_streams)):
        stream_to_file(in_streams[i], 'Anomalies_' + str(i + 1) + '_.txt')
    stream_to_file(regional_anomalies, 'regional_anomalies.txt')


if __name__ == '__main__':
    # dictionary of sensors and their source files
    sensors = \
        {
            'S1':
                {
                    'e': 'S1.e.txt',
                    'n': 'S1.n.txt',
                    'z': 'S1.z.txt'
                },
            'S2':
                {
                    'e': 'S2.e.txt',
                    'n': 'S2.n.txt',
                    'z': 'S2.z.txt'
                },
            'S3':
                {
                    'e': 'S3.e.txt',
                    'n': 'S3.n.txt',
                    'z': 'S3.z.txt'
                }
        }

    # 'e' for east, 'n' for north, 'z' for vertical
    directions = ['e', 'n', 'z']

    # source_sensor_direction is a dict {sensor name: {direction: source function}}
    # where source function is a function that copies data to stream
    sensor_source_dict = {}
    for sensor_name in sensors.keys():
        sensor_source_dict[sensor_name] = {}
        for direction in directions:
            # function for source must have two arguments: process and name of the stream
            def source_thread_target(proc, stream_name, filename=sensors[sensor_name][direction]):
                with open(filename, 'r') as fpin:
                    data = list(map(float, fpin))
                    for i in range(0, len(data), WINDOW_SIZE):
                        window = data[i:i+WINDOW_SIZE]
                        copy_data_to_stream(window, proc, stream_name)
                        time.sleep(TIME_INTERVAL)
                return
            sensor_source_dict[sensor_name][direction] = source_thread_target

    processes = {}
    # define processes for all the sensors
    for sensor_name in sensors.keys():
        processes[sensor_name + '_process'] = \
            {
                'in_stream_names_types': [('in_' + direction, 'f') for direction in directions],
                'out_stream_names_types': [('out', 'f')],
                'compute_func': f,
                'sources':
                    {
                        'source_' + direction:
                            {
                                'type': 'f',
                                'func': sensor_source_dict[sensor_name][direction]
                            }
                        for direction in directions
                    },
                'actuators': {}
            }

    # define the aggregation process
    processes['aggregation_process'] = \
        {
            'in_stream_names_types': [('in_' + sensor_name, 'f') for sensor_name in sensors.keys()],
            'out_stream_names_types': [],
            'compute_func': g,
            'sources': {},
            'actuators': {}
        }

    # make connections between processes
    connections = {}
    for sensor_name in sensors.keys():
        process_name = sensor_name + '_process'
        connections[process_name] = \
            {
                'out': [('aggregation_process', 'in_' + sensor_name)]
            }
        for direction in directions:
            connections[process_name]['source_' + direction] = [(process_name, 'in_' + direction)]

    connections['aggregation_process'] = {}

    multicore(processes, connections)
