use std::collections::BTreeMap;
use void::{Void, ResultVoidExt};
use sys::gpu::pstate;
use sys;
use types::{Microvolts, MicrovoltsDelta, Kilohertz, KilohertzDelta, Percentage, Range, Delta, RawConversion};
use clock::ClockDomain;

pub use sys::gpu::pstate::{PstateId as PState, VoltageInfoDomain as VoltageDomain, UtilizationDomain};

#[derive(Debug, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub struct PStateSettings {
    pub id: PState,
    pub editable: bool,
    pub clocks: Vec<ClockEntry>,
    pub base_voltages: Vec<BaseVoltage>,
}

#[derive(Debug, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub struct PStates {
    pub editable: bool,
    pub pstates: Vec<PStateSettings>,
    pub overvolt: Vec<BaseVoltage>,
}

#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub enum ClockEntry {
    Single {
        domain: ClockDomain,
        editable: bool,
        frequency_delta: Delta<KilohertzDelta>,
        frequency: Kilohertz,
    },
    Range {
        domain: ClockDomain,
        editable: bool,
        frequency_delta: Delta<KilohertzDelta>,
        frequency_range: Range<Kilohertz>,
        voltage_domain: VoltageDomain,
        voltage_range: Range<Microvolts>,
    },
}

impl ClockEntry {
    pub fn domain(&self) -> ClockDomain {
        match *self {
            ClockEntry::Single { domain, .. } => domain,
            ClockEntry::Range { domain, .. } => domain,
        }
    }

    pub fn editable(&self) -> bool {
        match *self {
            ClockEntry::Single { editable, .. } => editable,
            ClockEntry::Range { editable, .. } => editable,
        }
    }

    pub fn frequency_delta(&self) -> Delta<KilohertzDelta> {
        match *self {
            ClockEntry::Single { frequency_delta, .. } => frequency_delta,
            ClockEntry::Range { frequency_delta, .. } => frequency_delta,
        }
    }
}

#[derive(Debug, Copy, Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub struct BaseVoltage {
    pub voltage_domain: VoltageDomain,
    pub editable: bool,
    pub voltage: Microvolts,
    pub voltage_delta: Delta<MicrovoltsDelta>,
}

impl PStateSettings {
    pub fn from_raw(settings: &pstate::NV_GPU_PERF_PSTATES20_PSTATE, num_clocks: usize, num_base_voltages: usize) -> Result<Self, sys::ArgumentRangeError> {
        trace!("convert_raw({:#?}, {:?}, {:?})", settings, num_clocks, num_base_voltages);
        Ok(PStateSettings {
            id: PState::from_raw(settings.pstateId)?,
            editable: settings.bIsEditable.get(),
            clocks: settings.clocks[..num_clocks].iter().map(RawConversion::convert_raw).collect::<Result<_, _>>()?,
            base_voltages: settings.baseVoltages[..num_base_voltages].iter().map(RawConversion::convert_raw).collect::<Result<_, _>>()?,
        })
    }
}

impl RawConversion for pstate::NV_GPU_PERF_PSTATES20_INFO_V2 {
    type Target = PStates;
    type Error = sys::ArgumentRangeError;

    fn convert_raw(&self) -> Result<Self::Target, Self::Error> {
        trace!("convert_raw({:#?})", self);
        Ok(PStates {
            editable: self.bIsEditable.get(),
            pstates: self.pstates[..self.numPstates as usize].iter().map(|ps| PStateSettings::from_raw(ps, self.numClocks as _, self.numBaseVoltages as _)).collect::<Result<_, _>>()?,
            overvolt: self.voltages[..self.numVoltages as usize].iter().map(RawConversion::convert_raw).collect::<Result<_, _>>()?,
        })
    }
}

impl RawConversion for pstate::NV_GPU_PERF_PSTATE20_BASE_VOLTAGE_ENTRY_V1 {
    type Target = BaseVoltage;
    type Error = sys::ArgumentRangeError;

    fn convert_raw(&self) -> Result<Self::Target, Self::Error> {
        trace!("convert_raw({:#?})", self);
        Ok(BaseVoltage {
            voltage_domain: VoltageDomain::from_raw(self.domainId)?,
            editable: self.bIsEditable.get(),
            voltage: Microvolts(self.volt_uV),
            voltage_delta: match self.voltDelta_uV.convert_raw().void_unwrap() {
                Delta { value, range } => Delta {
                    value: MicrovoltsDelta(value.0),
                    range: Range {
                        min: MicrovoltsDelta(range.min.0),
                        max: MicrovoltsDelta(range.max.0),
                    },
                },
            },
        })
    }
}

impl RawConversion for pstate::NV_GPU_PSTATE20_CLOCK_ENTRY_V1 {
    type Target = ClockEntry;
    type Error = sys::ArgumentRangeError;

    fn convert_raw(&self) -> Result<Self::Target, Self::Error> {
        trace!("convert_raw({:#?})", self);
        Ok(match self.data.get(pstate::PstateClockType::from_raw(self.typeId)?) {
            pstate::NV_GPU_PSTATE20_CLOCK_ENTRY_DATA_VALUE::Single(single) => ClockEntry::Single {
                domain: ClockDomain::from_raw(self.domainId)?,
                editable: self.bIsEditable.get(),
                frequency_delta: self.freqDelta_kHz.convert_raw().void_unwrap(),
                frequency: Kilohertz(single.freq_kHz),
            },
            pstate::NV_GPU_PSTATE20_CLOCK_ENTRY_DATA_VALUE::Range(range) => ClockEntry::Range {
                domain: ClockDomain::from_raw(self.domainId)?,
                editable: self.bIsEditable.get(),
                frequency_delta: self.freqDelta_kHz.convert_raw().void_unwrap(),
                frequency_range: Range {
                    min: Kilohertz(range.minFreq_kHz),
                    max: Kilohertz(range.maxFreq_kHz),
                },
                voltage_domain: VoltageDomain::from_raw(range.domainId)?,
                voltage_range: Range {
                    min: Microvolts(range.minVoltage_uV),
                    max: Microvolts(range.maxVoltage_uV),
                },
            },
        })
    }
}

impl RawConversion for pstate::NV_GPU_PERF_PSTATES20_PARAM_DELTA {
    type Target = Delta<KilohertzDelta>;
    type Error = Void;

    fn convert_raw(&self) -> Result<Self::Target, Self::Error> {
        trace!("convert_raw({:#?})", self);
        Ok(Delta {
            value: KilohertzDelta(self.value),
            range: Range {
                min: KilohertzDelta(self.min),
                max: KilohertzDelta(self.max),
            },
        })
    }
}

impl RawConversion for pstate::NV_GPU_DYNAMIC_PSTATES_INFO_EX {
    type Target = BTreeMap<pstate::UtilizationDomain, Percentage>;
    type Error = sys::ArgumentRangeError;

    fn convert_raw(&self) -> Result<Self::Target, Self::Error> {
        trace!("convert_raw({:#?})", self);
        if self.flag_enabled() {
            Ok(BTreeMap::new())
        } else {
            pstate::UtilizationDomain::values()
                .map(|domain| (domain, &self.utilization[domain.raw() as usize]))
                .filter(|&(_, util)| util.bIsPresent.get())
                .map(|(id, util)| Percentage::from_raw(util.percentage).map(|p| (id, p)))
                .collect()
        }
    }
}
