"""Useful functions for processing and analyzing circadian data"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/07_utils.ipynb.

# %% auto 0
__all__ = ['phase_difference', 'amplitude_percent_change', 'sleep_midpoint_and_duration', 'utc_to_hrs', 'phase_ic_guess',
           'abs_hour_diff', 'cut_phases_12', 'convert_binary', 'cal_days_diff', 'parse_dt', 'phase_coherence',
           'phase_coherence_clock', 'angle_difference', 'subtract_clock_times', 'times_to_angle']

# %% ../nbs/api/07_utils.ipynb 4
import scipy as sp
import numpy as np
import pandas as pd
from typing import List
from scipy.stats import circmean

# %% ../nbs/api/07_utils.ipynb 5
def phase_difference(phase_1: float, # phase between [-pi, pi]
                     phase_2: float # phase between [-pi, pi]
                     ) -> float:
    "Phase difference (phase_2 - phase_1). Negative values represent clockwise rotations"
    diff = phase_2 - phase_1
    normalized_diff = (diff + np.pi) % (2 * np.pi) - np.pi
    return normalized_diff

# %% ../nbs/api/07_utils.ipynb 7
def amplitude_percent_change(amplitude_1: float, # amplitude between (0, inf)
                             amplitude_2: float # amplitude between (0, inf)
                             ):
    "Percent change between two amplitudes"
    return (amplitude_2 - amplitude_1) / amplitude_1 * 100

# %% ../nbs/api/07_utils.ipynb 9
def sleep_midpoint_and_duration(
    time: np.ndarray, # array of time values
    sleep_state: np.ndarray, # array of sleep state values
) -> List[np.ndarray]:
    "Calculate sleep duration and mid-sleep time"
    if not isinstance(time, np.ndarray):
        time = np.array(time)
        raise ValueError("time must be a numpy array")
    if not isinstance(sleep_state, np.ndarray):
        sleep_state = np.array(sleep_state)
        raise ValueError("sleep_state must be a numpy array")
    if len(time) != len(sleep_state):
        raise ValueError("time and sleep_state must have the same length")

    sleep_start_idxs = np.where(np.diff(sleep_state) == 1)[0]
    sleep_end_idxs = np.where(np.diff(sleep_state) == -1)[0] 
    # trim any incomplete sleep windows
    if sleep_start_idxs[0] > sleep_end_idxs[0]:
        sleep_end_idxs = sleep_end_idxs[1:]
    if sleep_start_idxs[-1] > sleep_end_idxs[-1]:
        sleep_start_idxs = sleep_start_idxs[:-1]

    sleep_duration = np.mean(time[sleep_end_idxs] - time[sleep_start_idxs])
    sleep_midpoints = (time[sleep_start_idxs] + time[sleep_end_idxs]) / 2.0
    mid_sleep_time = circmean(np.mod(sleep_midpoints, 24.0), high=24.0)

    return mid_sleep_time, sleep_duration

# %% ../nbs/api/07_utils.ipynb 11
def utc_to_hrs(d: pd.Timestamp # UTC timestamp to convert
               ) -> float: # hours since midnight
    "Convert UTC timestamp to hours since midnight"
    return d.hour + d.minute / 60.0 + d.second / 3600.0

# %% ../nbs/api/07_utils.ipynb 13
def phase_ic_guess(time_of_day: float) -> float:
    time_of_day = np.fmod(time_of_day, 24.0)

    # Wake at 8 am after 8 hours of sleep
    # State at 00:00
    psi = 1.65238233

    # Convert to radians, add to phase
    psi += time_of_day * np.pi / 12
    return psi


def abs_hour_diff(x, y):
    "Find the difference in hours between two clock times (wrapped)"
    a1 = min(x, y)
    a2 = max(x, y)
    s1 = a2-a1
    s2 = 24.0+a1-a2
    return(min(s1, s2))


def cut_phases_12(p):
    """
    Function to make the branch cut for the DLMO times be at 12 instead of 24.
    This is better because lots of DLMOs are near midnight, but many fewer are near
    noon.

        cut_phases_12(timept)
    """

    while (p < 0.0):
        p += 24.0

    p = np.fmod(p, 24.0)

    if p > 12:
        return p-24.0
    else:
        return p


def convert_binary(x, breakpoint: float = 0.50):
    x[x <= breakpoint] = 0.0
    x[x > breakpoint] = 1.1
    return x


def cal_days_diff(a, b):
    "Get the calander days between two time dates"
    A = a.replace(hour=0, minute=0, second=0, microsecond=0)
    B = b.replace(hour=0, minute=0, second=0, microsecond=0)
    return (A - B).days


def parse_dt(date, time):
    strDate = date + ' ' + time
    return pd.to_datetime(strDate, format='%m/%d/%Y %I:%M %p')

# %% ../nbs/api/07_utils.ipynb 14
def phase_coherence(series):
    Z=complex(0,0)
    series=np.array(series)
    for i in range(len(series)):
        Z+=np.exp(series[i]*complex(0,1))

    Z=Z/float(len(series))

    ans=np.absolute(Z)
    return(ans)


def phase_coherence_clock(series):
    angles=np.pi/12.0*series
    return(phase_coherence(angles))


def angle_difference(c1, c2) -> float:
    "Find the angle between two angles given in radians"
    return(np.angle(np.exp(complex(0,1)*(c1-c2))))


def subtract_clock_times(c1, c2):
    "Find the hour differences between two clock times new"
    a1=sp.pi/12.0*c1
    a2=sp.pi/12.0*c2
    adiff=angle_difference(a1, a2)
    return(12.0/sp.pi*adiff)

# %% ../nbs/api/07_utils.ipynb 15
def times_to_angle(time_vector: np.ndarray):
    "Take an array of times and return R, psi giving the mean angle (psi) and amplitude (R)"
    rad_vector = np.fmod(time_vector, 24.0) * np.pi/12.0
    Z = np.sum(np.exp(rad_vector*1j))/len(rad_vector)
    return(np.abs(Z), np.angle(Z))
