// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::cmp::Ordering;
use std::mem::MaybeUninit;
use std::ptr;

use super::win_bindings::{GetTimeZoneInformationForYear, SYSTEMTIME, TIME_ZONE_INFORMATION};

use crate::offset::local::{lookup_with_dst_transitions, Transition};
use crate::{Datelike, FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, NaiveTime, Weekday};

// We don't use `SystemTimeToTzSpecificLocalTime` because it doesn't support the same range of dates
// as Chrono. Also it really isn't that difficult to work out the correct offset from the provided
// DST rules.
//
// This method uses `overflowing_sub_offset` because it is no problem if the transition time in UTC
// falls a couple of hours inside the buffer space around the `NaiveDateTime` range (although it is
// very theoretical to have a transition at midnight around `NaiveDate::(MIN|MAX)`.
pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
    // Using a `TzInfo` based on the year of an UTC datetime is technically wrong, we should be
    // using the rules for the year of the corresponding local time. But this matches what
    // `SystemTimeToTzSpecificLocalTime` is documented to do.
    let tz_info = match TzInfo::for_year(utc.year()) {
        Some(tz_info) => tz_info,
        None => return MappedLocalTime::None,
    };
    let offset = match (tz_info.std_transition, tz_info.dst_transition) {
        (Some(std_transition), Some(dst_transition)) => {
            let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
            let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
            if dst_transition_utc < std_transition_utc {
                match utc >= &dst_transition_utc && utc < &std_transition_utc {
                    true => tz_info.dst_offset,
                    false => tz_info.std_offset,
                }
            } else {
                match utc >= &std_transition_utc && utc < &dst_transition_utc {
                    true => tz_info.std_offset,
                    false => tz_info.dst_offset,
                }
            }
        }
        (Some(std_transition), None) => {
            let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset);
            match utc < &std_transition_utc {
                true => tz_info.dst_offset,
                false => tz_info.std_offset,
            }
        }
        (None, Some(dst_transition)) => {
            let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset);
            match utc < &dst_transition_utc {
                true => tz_info.std_offset,
                false => tz_info.dst_offset,
            }
        }
        (None, None) => tz_info.std_offset,
    };
    MappedLocalTime::Single(offset)
}

// We don't use `TzSpecificLocalTimeToSystemTime` because it doesn't let us choose how to handle
// ambiguous cases (during a DST transition). Instead we get the timezone information for the
// current year and compute it ourselves, like we do on Unix.
pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime<FixedOffset> {
    let tz_info = match TzInfo::for_year(local.year()) {
        Some(tz_info) => tz_info,
        None => return MappedLocalTime::None,
    };
    // Create a sorted slice of transitions and use `lookup_with_dst_transitions`.
    match (tz_info.std_transition, tz_info.dst_transition) {
        (Some(std_transition), Some(dst_transition)) => {
            let std_transition =
                Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset);
            let dst_transition =
                Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset);
            let transitions = match std_transition.cmp(&dst_transition) {
                Ordering::Less => [std_transition, dst_transition],
                Ordering::Greater => [dst_transition, std_transition],
                Ordering::Equal => {
                    // This doesn't make sense. Let's just return the standard offset.
                    return MappedLocalTime::Single(tz_info.std_offset);
                }
            };
            lookup_with_dst_transitions(&transitions, *local)
        }
        (Some(std_transition), None) => {
            let transitions =
                [Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset)];
            lookup_with_dst_transitions(&transitions, *local)
        }
        (None, Some(dst_transition)) => {
            let transitions =
                [Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset)];
            lookup_with_dst_transitions(&transitions, *local)
        }
        (None, None) => MappedLocalTime::Single(tz_info.std_offset),
    }
}

// The basis for Windows timezone and DST support has been in place since Windows 2000. It does not
// allow for complex rules like the IANA timezone database:
// - A timezone has the same base offset the whole year.
// - There seem to be either zero or two DST transitions (but we support having just one).
// - As of Vista(?) only years from 2004 until a few years into the future are supported.
// - All other years get the base settings, which seem to be that of the current year.
//
// These details don't matter much, we just work with the offsets and transition dates Windows
// returns through `GetTimeZoneInformationForYear` for a particular year.
struct TzInfo {
    // Offset from UTC during standard time.
    std_offset: FixedOffset,
    // Offset from UTC during daylight saving time.
    dst_offset: FixedOffset,
    // Transition from standard time to daylight saving time, given in local standard time.
    std_transition: Option<NaiveDateTime>,
    // Transition from daylight saving time to standard time, given in local daylight saving time.
    dst_transition: Option<NaiveDateTime>,
}

impl TzInfo {
    fn for_year(year: i32) -> Option<TzInfo> {
        // The API limits years to 1601..=30827.
        // Working with timezones and daylight saving time this far into the past or future makes
        // little sense. But whatever is extrapolated for 1601 or 30827 is what can be extrapolated
        // for years beyond.
        let ref_year = year.clamp(1601, 30827) as u16;
        let tz_info = unsafe {
            let mut tz_info = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit();
            if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 {
                return None;
            }
            tz_info.assume_init()
        };
        let std_offset = (tz_info.Bias)
            .checked_add(tz_info.StandardBias)
            .and_then(|o| o.checked_mul(60))
            .and_then(FixedOffset::west_opt)?;
        let dst_offset = (tz_info.Bias)
            .checked_add(tz_info.DaylightBias)
            .and_then(|o| o.checked_mul(60))
            .and_then(FixedOffset::west_opt)?;
        Some(TzInfo {
            std_offset,
            dst_offset,
            std_transition: naive_date_time_from_system_time(tz_info.StandardDate, year).ok()?,
            dst_transition: naive_date_time_from_system_time(tz_info.DaylightDate, year).ok()?,
        })
    }
}

/// Resolve a `SYSTEMTIME` object to an `Option<NaiveDateTime>`.
///
/// A `SYSTEMTIME` within a `TIME_ZONE_INFORMATION` struct can be zero to indicate there is no
/// transition.
/// If it has year, month and day values it is a concrete date.
/// If the year is missing the `SYSTEMTIME` is a rule, which this method resolves for the provided
/// year. A rule has a month, weekday, and nth weekday of the month as components.
///
/// Returns `Err` if any of the values is invalid, which should never happen.
fn naive_date_time_from_system_time(
    st: SYSTEMTIME,
    year: i32,
) -> Result<Option<NaiveDateTime>, ()> {
    if st.wYear == 0 && st.wMonth == 0 {
        return Ok(None);
    }
    let time = NaiveTime::from_hms_milli_opt(
        st.wHour as u32,
        st.wMinute as u32,
        st.wSecond as u32,
        st.wMilliseconds as u32,
    )
    .ok_or(())?;

    if st.wYear != 0 {
        // We have a concrete date.
        let date =
            NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32).ok_or(())?;
        return Ok(Some(date.and_time(time)));
    }

    // Resolve a rule with month, weekday, and nth weekday of the month to a date in the current
    // year.
    let weekday = match st.wDayOfWeek {
        0 => Weekday::Sun,
        1 => Weekday::Mon,
        2 => Weekday::Tue,
        3 => Weekday::Wed,
        4 => Weekday::Thu,
        5 => Weekday::Fri,
        6 => Weekday::Sat,
        _ => return Err(()),
    };
    let nth_day = match st.wDay {
        1..=5 => st.wDay as u8,
        _ => return Err(()),
    };
    let date = NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, nth_day)
        .or_else(|| NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, 4))
        .ok_or(())?; // `st.wMonth` must be invalid
    Ok(Some(date.and_time(time)))
}

#[cfg(test)]
mod tests {
    use crate::offset::local::win_bindings::{
        SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime, FILETIME, SYSTEMTIME,
    };
    use crate::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeDelta};
    use crate::{Datelike, TimeZone, Timelike};
    use std::mem::MaybeUninit;
    use std::ptr;

    #[test]
    fn verify_against_tz_specific_local_time_to_system_time() {
        // The implementation in Windows itself is the source of truth on how to work with the OS
        // timezone information. This test compares for every hour over a period of 125 years our
        // implementation to `TzSpecificLocalTimeToSystemTime`.
        //
        // This uses parts of a previous Windows `Local` implementation in chrono.
        fn from_local_time(dt: &NaiveDateTime) -> DateTime<Local> {
            let st = system_time_from_naive_date_time(dt);
            let utc_time = local_to_utc_time(&st);
            let utc_secs = system_time_as_unix_seconds(&utc_time);
            let local_secs = system_time_as_unix_seconds(&st);
            let offset = (local_secs - utc_secs) as i32;
            let offset = FixedOffset::east_opt(offset).unwrap();
            DateTime::from_naive_utc_and_offset(*dt - offset, offset)
        }
        fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME {
            SYSTEMTIME {
                // Valid values: 1601-30827
                wYear: dt.year() as u16,
                // Valid values:1-12
                wMonth: dt.month() as u16,
                // Valid values: 0-6, starting Sunday.
                // NOTE: enum returns 1-7, starting Monday, so we are
                // off here, but this is not currently used in local.
                wDayOfWeek: dt.weekday() as u16,
                // Valid values: 1-31
                wDay: dt.day() as u16,
                // Valid values: 0-23
                wHour: dt.hour() as u16,
                // Valid values: 0-59
                wMinute: dt.minute() as u16,
                // Valid values: 0-59
                wSecond: dt.second() as u16,
                // Valid values: 0-999
                wMilliseconds: 0,
            }
        }
        fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME {
            let mut sys_time = MaybeUninit::<SYSTEMTIME>::uninit();
            unsafe { TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()) };
            // SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can
            // assume the value is initialized.
            unsafe { sys_time.assume_init() }
        }
        const HECTONANOSECS_IN_SEC: i64 = 10_000_000;
        const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC;
        fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 {
            let mut init = MaybeUninit::<FILETIME>::uninit();
            unsafe {
                SystemTimeToFileTime(st, init.as_mut_ptr());
            }
            // SystemTimeToFileTime must have succeeded at this point, so we can assume the value is
            // initialized.
            let filetime = unsafe { init.assume_init() };
            let bit_shift =
                ((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64);
            (bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC
        }

        let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 30, 0).unwrap();

        while date.year() < 2078 {
            // Windows doesn't handle non-existing dates, it just treats it as valid.
            if let Some(our_result) = Local.from_local_datetime(&date).earliest() {
                assert_eq!(from_local_time(&date), our_result);
            }
            date += TimeDelta::try_hours(1).unwrap();
        }
    }
}
