use std::collections::HashMap;

use chrono::{DateTime, Duration, Timelike, Utc};

use crate::clock;

/// A forecast is used to divine specific weather patterns for a zone.
#[derive(Debug)]
pub struct Forecast {
    pub zone_id: u32,
    pub rates: Vec<Rate>,
}

/// A weather rate consists of the Weather ID and the likelihood in which it occurs (also called "target").
/// The weather algorithm that the game uses generates a random integer between 0 and 100, and whichever rate
/// first satisfies the "is larger than target" condition gets selected.
/// This means that rates are:
/// 1. Sorted ascending by rate
/// 2. The larger the rate, the less likely the weather is to occur, statistically
#[derive(Debug)]
pub struct Rate {
    pub weather_id: u32,
    pub rate: u32,
}

/// A hash map of zones, indexed by "rate ID", which is unique in our source data.
pub type ForecastSet = HashMap<u32, Forecast>;

impl Forecast {
    /// Calculates the current weather for the given target magic number.
    pub fn weather_for_target(&self, target: u32) -> &Rate {
        // TODO: Don't unwrap here!
        self.rates.iter().find(|r| target < r.rate).unwrap()
    }

    /// Calculates the weather right now.
    pub fn weather_now(&self) -> &Rate {
        let utc = Utc::now();
        let target = calculate_target(utc);
        self.weather_for_target(target)
    }

    /// Calculates the weather at a specific point in Eorzean time.
    pub fn weather_at(&self, date: DateTime<Utc>) -> &Rate {
        let new_date = round_to_last_weather_time(date);
        let target = calculate_target(clock::to_earth_time(new_date));
        self.weather_for_target(target)
    }
}

/// Rounds to the last weather "start". These happen three times a day, at 0:00,
/// at 8:00, and at 16:00.
pub fn round_to_last_weather_time(date: DateTime<Utc>) -> DateTime<Utc> {
    let cur_hour = date.hour();
    // This essentially performs division without rest.
    let last_hour = (cur_hour / 8) * 8;
    date.date_naive()
        .and_hms_opt(last_hour, 0, 0)
        .unwrap()
        .and_utc()
}

/// Adds or subtracts `n` intervals of 8 hours to the last weather time.
pub fn modify_weather_time(n: i32) -> DateTime<Utc> {
    let last_weather_time = round_to_last_weather_time(clock::get_current_eorzea_date());
    let hours = n * 8;
    let td = Duration::hours(hours.abs() as i64);
    if hours > 0 {
        last_weather_time.checked_add_signed(td).unwrap()
    } else {
        last_weather_time.checked_sub_signed(td).unwrap()
    }
}

/// Calculates the magic target number for a specific date.
/// It's important to note that this expects a human time, not an Eorzean time.
pub fn calculate_target(m: DateTime<Utc>) -> u32 {
    let unix_time = m.timestamp().abs();
    let ez_hour = unix_time / 175;
    let inc = (ez_hour + 8 - (ez_hour % 8)) % 24;
    let total_days = unix_time / 4200;
    let calc_base: i32 = ((total_days * 100) + inc) as i32; // Needs to be i32 to assure correct shifting overflow!
    let step1 = ((calc_base << 11) ^ calc_base) as u32; // Cast to u32 here to literally interpret the two's complement, I suppose
    let step2 = (step1 >> 8) ^ step1;

    step2 % 100
}
