// Copyright 2024 The Drasi Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use rand::{
    distributions::{Distribution, Standard},
    Rng, SeedableRng,
};
use rand_pcg::Pcg64;
use std::sync::Arc;

use drasi_core::models::SourceChange;

use crate::scenario::SourceChangeGenerator;
use building_comfort_model::{BuildingComfortModel, Location, RoomProperty};

pub mod building_comfort_model;
pub mod single_node_calculation;
pub mod single_node_property_projection;
pub mod single_path_averaging;
pub mod single_path_no_change_averaging;

// A function to generate an array of building sizes. It will contain building_count
// entries. Each entry is a tuple of (building_idx, floors_per_building, rooms_per_floor). The values are
// generated by calling generate_building_size.
fn generate_building_sizes(
    building_count: usize,
    building_size_templates: &[(usize, usize)],
) -> Vec<(usize, usize, usize)> {
    let mut building_sizes = Vec::new();
    for building_idx in 0..building_count {
        building_sizes.push(generate_building_size(
            building_idx,
            building_size_templates,
        ));
    }
    building_sizes
}

// A function to determine the number of floors in a building and the number of rooms per floor.
// It uses values in building_size_templates to determine the values to use. If building_size_templates
// contains a single value, it is used for all buildings, otherwise the values are used in order. If
// there are more buildings than values in building_size_templates, the values are reused.
fn generate_building_size(
    building_idx: usize,
    building_size_templates: &[(usize, usize)],
) -> (usize, usize, usize) {
    let building_size_idx = building_idx % building_size_templates.len();
    let (floors_per_building, rooms_per_floor) = building_size_templates[building_size_idx];
    (building_idx, floors_per_building, rooms_per_floor)
}

// A minimal struct to capture the min and max of a time range
#[derive(Debug)]
pub struct TimeRange {
    pub min: u64,
    pub max: u64,
}

impl TimeRange {
    pub fn new(min: u64, max: u64) -> TimeRange {
        // Throw error if min is greater than max.
        if min > max {
            panic!("TimeRange min is greater than max.");
        }

        TimeRange { min, max }
    }
}

// A minimal struct to capture the min and max of a float range
#[derive(Debug)]
pub struct FloatRange {
    pub min: f32,
    pub max: f32,
}

impl FloatRange {
    pub fn new(min: f32, max: f32) -> FloatRange {
        // Throw error if min is greater than max.
        if min > max {
            panic!("FloatRange min is greater than max.");
        }

        FloatRange { min, max }
    }
}

#[derive(Debug)]
pub enum RoomChangeType {
    Temperature,
    Humidity,
    Co2,
}

// Implement Distribution for RoomChangeType so we can generate a random change type.
impl Distribution<RoomChangeType> for Standard {
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> RoomChangeType {
        match rng.gen_range(0..=2) {
            0 => RoomChangeType::Temperature,
            1 => RoomChangeType::Humidity,
            _ => RoomChangeType::Co2,
        }
    }
}

pub struct RoomPropertySourceChangeGeneratorConfig {
    iterations: u128,
    start_time_ms: u64,
    time_delta_range: TimeRange,
    seed: u64,
    temperature_delta_range: FloatRange,
    humidity_delta_range: FloatRange,
    co2_delta_range: FloatRange,
}

impl RoomPropertySourceChangeGeneratorConfig {
    pub fn new(
        iterations: u128,
        start_time_ms: u64,
        time_delta_range: TimeRange,
        seed: u64,
        temperature_delta_range: FloatRange,
        humidity_delta_range: FloatRange,
        co2_delta_range: FloatRange,
    ) -> RoomPropertySourceChangeGeneratorConfig {
        RoomPropertySourceChangeGeneratorConfig {
            iterations,
            start_time_ms,
            time_delta_range,
            seed,
            temperature_delta_range,
            humidity_delta_range,
            co2_delta_range,
        }
    }
}

struct RoomPropertySourceChangeGeneratorState {
    iterations_complete: u128,
    current_time_ms: u64,
}

pub struct RoomPropertySourceChangeGenerator {
    model: Arc<BuildingComfortModel>,
    config: RoomPropertySourceChangeGeneratorConfig,
    rng: Pcg64,
    state: RoomPropertySourceChangeGeneratorState,
}

impl RoomPropertySourceChangeGenerator {
    pub fn new(
        model: Arc<BuildingComfortModel>,
        config: RoomPropertySourceChangeGeneratorConfig,
    ) -> RoomPropertySourceChangeGenerator {
        let state = RoomPropertySourceChangeGeneratorState {
            iterations_complete: 0,
            current_time_ms: config.start_time_ms,
        };

        let rng = Pcg64::seed_from_u64(config.seed);

        RoomPropertySourceChangeGenerator {
            model,
            config,
            rng,
            state,
        }
    }

    // A function to generate a random room location the vector of Building Sizes. Each building size is
    // a tuple of (building_idx, floors_per_building, rooms_per_floor). The function will pick a random building, floor, and
    // room within the building size constraints and return a Location.
    fn generate_random_room_location(&mut self) -> Location {
        let building_idx = self.rng.gen_range(0..self.model.get_building_count());
        let (floors_per_building, rooms_per_floor) = self.model.get_building_size(building_idx);
        let floor_idx = self.rng.gen_range(0..floors_per_building);
        let room_idx = self.rng.gen_range(0..rooms_per_floor);
        Location::new_room(building_idx, floor_idx, room_idx)
    }
}

#[allow(clippy::unwrap_used)]
impl SourceChangeGenerator for RoomPropertySourceChangeGenerator {
    fn generate_change(&mut self) -> Option<SourceChange> {
        if self.state.iterations_complete >= self.config.iterations {
            return None;
        }

        // Increment the current_time_ms time for the new change upto the maximum configured.
        let current_time_ms = self.state.current_time_ms
            + self
                .rng
                .gen_range(self.config.time_delta_range.min..=self.config.time_delta_range.max);
        let changing_room_location = self.generate_random_room_location();
        let change_type: RoomChangeType = self.rng.gen();

        let source_change = match change_type {
            RoomChangeType::Temperature => self.model.update_room_property(
                current_time_ms,
                changing_room_location,
                RoomProperty::Temperature,
                self.rng.gen_range(
                    self.config.temperature_delta_range.min
                        ..=self.config.temperature_delta_range.max,
                ),
            ),
            RoomChangeType::Humidity => self.model.update_room_property(
                current_time_ms,
                changing_room_location,
                RoomProperty::Humidity,
                self.rng.gen_range(
                    self.config.humidity_delta_range.min..=self.config.humidity_delta_range.max,
                ),
            ),
            RoomChangeType::Co2 => self.model.update_room_property(
                current_time_ms,
                changing_room_location,
                RoomProperty::Co2,
                self.rng
                    .gen_range(self.config.co2_delta_range.min..=self.config.co2_delta_range.max),
            ),
        };
        // println!("source_change: {:#?}", source_change);

        // Update the SourceChangeGeneratorState.
        self.state.iterations_complete += 1;
        self.state.current_time_ms = current_time_ms;

        Option::Some(SourceChange::Update {
            element: source_change.node_after.unwrap(),
        })
    }
}
