use std::cell::{Cell};
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::marker::PhantomData;
use crate::{Quota, QuotaRef, QuotaResult};
use crate::QuotaPolicy;
pub struct StaleQuotaRef;
pub struct QuotaPool<'a> {
    initial_available_tokens: u64,
    policy: QuotaPolicy,
    key_map: Cell<HashMap<&'static str, usize>>,
    quotas: Cell<Vec<Quota>>,
    last_access_times_ns: Cell<Vec<std::time::Instant>>,
    last_refill_times_ns: Cell<Vec<std::time::Instant>>,
    epoch: Cell<std::time::Instant>,
    generations: Cell<Vec<u32>>,
    occupied: Cell<Vec<bool>>,
    _marker: PhantomData<&'a ()>,
}

impl<'a> QuotaPool<'a> {
    #[inline]
    pub fn new(policy: QuotaPolicy, initial_available_tokens: u64) -> Self {
        Self {
            initial_available_tokens,
            policy,
            key_map: Cell::new(HashMap::new()),
            quotas: Cell::new(Vec::new()),
            last_access_times_ns: Cell::new(Vec::new()),
            last_refill_times_ns: Cell::new(Vec::new()),
            epoch: Cell::new(std::time::Instant::now()),
            generations: Cell::new(Vec::new()),
            occupied: Cell::new(Vec::new()),
            _marker: PhantomData,
        }
    }

    #[inline]
    pub fn start(&mut self) {
        let now = std::time::Instant::now();
        self.epoch = Cell::new(now);
        self.last_access_times_ns.get_mut().fill(now);
    }
/*
    #[inline]
    pub fn quota(&self, key: &'static str) -> &Quota {
        unsafe {
            let key_map = &mut *self.key_map.as_ptr();
            let quotas = &mut *self.quotas.as_ptr();
            match key_map.entry(key) {
                Entry::Occupied(occupied_entry) => {
                    &quotas[*occupied_entry.get()]
                }
                Entry::Vacant(vacant_entry) => {
                    let last_access_times_ns = &mut *self.last_access_times_ns.as_ptr();
                    let last_refill_times_ns = &mut *self.last_refill_times_ns.as_ptr();
                    let new_index = quotas.len();
                    vacant_entry.insert(new_index);
                    quotas.push(Quota::with_initial_tokens(self.initial_available_tokens));
                    let t = std::time::Instant::now();
                    last_access_times_ns.push(t);
                    last_refill_times_ns.push(t);
                    &quotas.get_unchecked(new_index)
                }
            }
        }
    }
*/

    #[inline]
    pub fn quota(&'a self, key: &'static str) -> QuotaRef<'a> {
        unsafe {
            let key_map = &mut *self.key_map.as_ptr();
            let quotas = &mut *self.quotas.as_ptr();
            match key_map.entry(key) {
                Entry::Occupied(occupied_entry) => {
                    QuotaRef {
                        pool: &self,
                        index: *occupied_entry.get(),
                        generation: 0
                    }
                }
                Entry::Vacant(vacant_entry) => {
                    let last_access_times_ns = &mut *self.last_access_times_ns.as_ptr();
                    let last_refill_times_ns = &mut *self.last_refill_times_ns.as_ptr();
                    let new_index = quotas.len();
                    vacant_entry.insert(new_index);
                    quotas.push(Quota::with_initial_tokens(self.initial_available_tokens));
                    let t = std::time::Instant::now();
                    last_access_times_ns.push(t);
                    last_refill_times_ns.push(t);
                    QuotaRef {
                        pool: &self,
                        index: new_index,
                        generation: 0
                    }
                }
            }
        }
    }
    #[inline]
    pub fn quota_with_index(&self, key: &'static str) -> (&Quota, usize) {
        unsafe {
            let key_map = &mut *self.key_map.as_ptr();
            let quotas = &mut *self.quotas.as_ptr();
            match key_map.entry(key) {
                Entry::Occupied(occupied_entry) => {
                    let i = *occupied_entry.get();
                    (&quotas[i], i)
                }
                Entry::Vacant(vacant_entry) => {
                    let last_access_times_ns = &mut *self.last_access_times_ns.as_ptr();
                    let last_refill_times_ns = &mut *self.last_refill_times_ns.as_ptr();
                    let new_index = quotas.len();
                    vacant_entry.insert(new_index);
                    quotas.push(Quota::with_initial_tokens(self.initial_available_tokens));
                    let t = std::time::Instant::now();
                    last_access_times_ns.push(t);
                    last_refill_times_ns.push(t);
                    (&quotas.get_unchecked(new_index), new_index)
                }
            }
        }
    }

    /// Ticks the Quota with the time difference since last access
    /// and then consumes the cost and returns the result
    #[inline]
    pub fn consume(&self, key: &'static str, cost: u64) -> QuotaResult {
        let (quota, index) = self.quota_with_index(key);
        let last_access_time_ns = unsafe { (&mut *self.last_access_times_ns.as_ptr())[index] };

        let delta = last_access_time_ns.elapsed();

        self.policy.tick(delta.as_nanos() as u64, &quota);
        quota.consume(cost)
    }


    #[inline]
    pub(crate) fn consume_at(
        &self,
        index: usize,
        generation: u32,
        cost: u64,
    ) -> Result<QuotaResult, StaleQuotaRef> {
        unsafe {
            let quotas = &mut *self.quotas.as_ptr();
            let generations = &*self.generations.as_ptr();
            let occupied = &*self.occupied.as_ptr();
            let last_access_times = &mut *self.last_access_times_ns.as_ptr();

            if index >= quotas.len()
                || generations[index] != generation
                || !occupied[index]
            {
                return Err(StaleQuotaRef);
            }

            let now = std::time::Instant::now();
            let dt = now.duration_since(last_access_times[index]);
            last_access_times[index] = now;

            self.policy.tick(dt.as_nanos() as u64, &quotas[index]);

            Ok(quotas[index].consume(cost))
        }
    }
}

#[cfg(test)]
mod tests;