use super::{try_join_all, Arc, FilteredDataValue, HashMap, Mutex, RealtimePriceData};

#[derive(Clone, Debug)]
pub struct AdxData {
    pub adx: f64,
    pub close_time: i64,
}

struct BasicData {
    dm_plus: f64,
    dm_minus: f64,
    true_range: f64,
    close_time: i64,
}

struct DiData {
    di_plus: f64,
    di_minus: f64,
    close_time: i64,
}

pub async fn adx(
    adx_len: usize,
    di_len: usize,
    input_rt_data: &HashMap<String, Vec<RealtimePriceData>>,
    filtered_symbols: &HashMap<String, FilteredDataValue>,
) -> Result<HashMap<String, Vec<AdxData>>, Box<dyn std::error::Error + Send + Sync>> {
    if filtered_symbols.is_empty() {
        Err(("Err"))?;
    }

    let mut adx_vec: HashMap<String, Vec<AdxData>> = HashMap::new();
    let mut adx_vec_arc = Arc::new(Mutex::new(adx_vec));
    let mut task_vec = Vec::new();
    for (filtered_symbol, filtered_data) in filtered_symbols {
        let mut rt_data_map = input_rt_data.clone();
        let adx_vec_arc_c = Arc::clone(&adx_vec_arc);
        let symbol = filtered_symbol.clone();
        task_vec.push(tokio::spawn(async move {
            if let Some(rt_price_data) = rt_data_map.get(&symbol) {
                if rt_price_data.len() - 1 > adx_len && rt_price_data.len() > di_len {
                    // step 1: calculate +DM, -DM, TR
                    let windows = rt_price_data.windows(2);
                    let mut basic_data_vec: Vec<BasicData> = Vec::new();
                    for window in windows {
                        let prev_rt_data = window.first().unwrap();
                        let current_rt_data = window.last().unwrap();
                        let mut up = current_rt_data.high_price - prev_rt_data.high_price;
                        let mut down = prev_rt_data.low_price - current_rt_data.low_price;
                        let basic_data = BasicData {
                            dm_plus: if up > down && up > 0.0 { up } else { 0.0 },
                            dm_minus: if down > up && down > 0.0 { down } else { 0.0 },
                            true_range: f64::max(
                                f64::max(
                                    current_rt_data.high_price - current_rt_data.low_price,
                                    current_rt_data.high_price - prev_rt_data.close_price,
                                ),
                                current_rt_data.low_price - prev_rt_data.close_price,
                            ),
                            close_time: current_rt_data.close_time,
                        };
                        basic_data_vec.push(basic_data);
                    }
                    // step 2: smoothing +DM, -DM, TR
                    let alpha: f64 = 1.0 / (di_len as f64);
                    let mut smoothed_basic_data_vec: Vec<BasicData> = Vec::new();

                    let partial_vec1 = basic_data_vec.get(..di_len).unwrap(); // for calculation of initial value
                    let partial_vec2 = basic_data_vec.get(di_len..).unwrap(); // for calculation of the rest

                    let mut dm_plus_calculated = 0.0;
                    let mut dm_minus_calculated = 0.0;
                    let mut tr_calculated = 0.0;
                    for element in partial_vec1 {
                        dm_plus_calculated += element.dm_plus;
                        dm_minus_calculated += element.dm_minus;
                        tr_calculated += element.true_range;
                    }
                    dm_plus_calculated /= di_len as f64;
                    dm_minus_calculated /= di_len as f64;
                    tr_calculated /= di_len as f64;

                    let basic_data = BasicData {
                        dm_plus: dm_plus_calculated,
                        dm_minus: dm_minus_calculated,
                        true_range: tr_calculated,
                        close_time: partial_vec1.last().unwrap().close_time,
                    };
                    smoothed_basic_data_vec.push(basic_data);

                    for element in partial_vec2 {
                        dm_plus_calculated =
                            alpha * element.dm_plus + (1.0 - alpha) * dm_plus_calculated;
                        dm_minus_calculated =
                            alpha * element.dm_minus + (1.0 - alpha) * dm_minus_calculated;
                        tr_calculated = alpha * element.true_range + (1.0 - alpha) * tr_calculated;
                        let basic_data = BasicData {
                            dm_plus: dm_plus_calculated,
                            dm_minus: dm_minus_calculated,
                            true_range: tr_calculated,
                            close_time: element.close_time,
                        };
                        smoothed_basic_data_vec.push(basic_data);
                    }

                    // step 3: calculate DI
                    let mut di_data_vec: Vec<DiData> = Vec::new();
                    for basic_data in smoothed_basic_data_vec {
                        let di_data = DiData {
                            di_plus: (100.0 * basic_data.dm_plus) / basic_data.true_range,
                            di_minus: (100.0 * basic_data.dm_minus) / basic_data.true_range,
                            close_time: basic_data.close_time,
                        };
                        di_data_vec.push(di_data);
                    }

                    // step 4: calculate ADX
                    let mut initial_adx_vec: Vec<AdxData> = Vec::new();
                    for di_data in di_data_vec {
                        let sum = di_data.di_plus + di_data.di_minus;
                        let difference = (di_data.di_plus - di_data.di_minus).abs();
                        let divisor = if sum <= 0.00000001 { 1.0 } else { sum };
                        let adx_data = AdxData {
                            adx: difference.abs() / divisor,
                            close_time: di_data.close_time,
                        };
                        initial_adx_vec.push(adx_data);
                    }

                    if let (Some(partial_vec1), Some(partial_vec2)) = (
                        initial_adx_vec.get(..adx_len),
                        initial_adx_vec.get(adx_len..),
                    ) {
                        // partial_vec1 is for calculation of initial value
                        // partial_vec2 is for calculation of the rest
                        let mut smoothed_adx_vec: Vec<AdxData> = Vec::new();
                        let mut adx_calculated = 0.0;
                        for element in partial_vec1 {
                            adx_calculated += element.adx;
                        }
                        adx_calculated /= adx_len as f64;

                        let adx_data = AdxData {
                            adx: adx_calculated,
                            close_time: partial_vec1.last().unwrap().close_time,
                        };
                        smoothed_adx_vec.push(adx_data);

                        let alpha: f64 = 1.0 / (adx_len as f64);
                        for element in partial_vec2 {
                            adx_calculated = alpha * element.adx + (1.0 - alpha) * adx_calculated;
                            let adx_data = AdxData {
                                adx: 100.0 * adx_calculated,
                                close_time: element.close_time,
                            };
                            smoothed_adx_vec.push(adx_data);
                        }

                        let mut adx_vec_arc_lock = adx_vec_arc_c.lock().await;
                        adx_vec_arc_lock.insert(symbol.clone(), smoothed_adx_vec.clone());
                    }
                }
            }
        }));
    }
    try_join_all(task_vec).await?;

    let a = adx_vec_arc.lock().await.to_owned();
    Ok(a)
}
