use super::base::IntegratorBase;
use super::RayIntegratorTrait;
use crate::core::camera::InitMetadata;
use crate::core::film::FilmTrait;
use crate::core::image::{Image, ImageIO, ImageMetadata};
use crate::globals::get_options;
use crate::spectra::get_spectra_context;
use crate::Arena;
use indicatif::{ProgressBar, ProgressStyle};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use shared::core::camera::{Camera, CameraTrait};
use shared::core::geometry::{Bounds2i, Point2i, VectorLike};
use shared::core::sampler::get_camera_sample;
use shared::core::sampler::{Sampler, SamplerTrait};
use shared::spectra::SampledSpectrum;
use shared::Float;
use std::io::Write;
use std::path::Path;
use std::sync::Arc;

struct PbrtProgress {
    bar: ProgressBar,
}

impl PbrtProgress {
    fn new(total_work: u64, description: &str, quiet: bool) -> Self {
        if quiet {
            return Self {
                bar: ProgressBar::hidden(),
            };
        }

        let bar = ProgressBar::new(total_work);

        bar.set_style(
            ProgressStyle::default_bar()
                .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}")
                .unwrap()
                .progress_chars("=>-"),
        );

        bar.set_message(description.to_string());

        Self { bar }
    }

    fn update(&self, amount: u64) {
        self.bar.inc(amount);
    }

    fn done(&self) {
        self.bar.finish_with_message("Done");
    }

    fn elapsed_seconds(&self) -> f32 {
        self.bar.elapsed().as_secs_f32()
    }
}

fn generate_tiles(bounds: Bounds2i) -> Vec<Bounds2i> {
    let mut tiles = Vec::new();
    const TILE_SIZE: i32 = 16;
    for y in (bounds.p_min.y()..bounds.p_max.y()).step_by(TILE_SIZE as usize) {
        for x in (bounds.p_min.x()..bounds.p_max.x()).step_by(TILE_SIZE as usize) {
            let p_min = Point2i::new(x, y);
            let p_max = Point2i::new(
                (x + TILE_SIZE).min(bounds.p_max.x()),
                (y + TILE_SIZE).min(bounds.p_max.y()),
            );
            tiles.push(Bounds2i::from_points(p_min, p_max));
        }
    }
    tiles
}

pub fn render<T>(
    integrator: &T,
    _base: &IntegratorBase,
    camera: &Camera,
    sampler_prototype: &Sampler,
    arena: Arc<Arena>,
) where
    T: RayIntegratorTrait + Sync,
{
    let options = get_options();
    if let Some((p_pixel, sample_index)) = options.debug_start {
        let s_index = sample_index as usize;
        let mut tile_sampler = sampler_prototype.clone();

        tile_sampler.start_pixel_sample(p_pixel, s_index as i32, None);

        evaluate_pixel_sample(
            integrator,
            camera,
            &mut tile_sampler,
            p_pixel,
            s_index,
            &arena,
        );
        return;
    }

    let pixel_bounds = camera.get_film().pixel_bounds();
    let spp = sampler_prototype.samples_per_pixel();
    let total_work = (pixel_bounds.area() as u64) * (spp as u64);
    let progress = PbrtProgress::new(total_work, "Rendering", options.quiet);
    let mut wave_start = 0;
    let mut wave_end = 1;
    let mut next_wave_size = 1;

    let mut reference_image: Option<Image> = None;
    let mut mse_out_file: Option<std::fs::File> = None;

    if let Some(ref_path) = &options.mse_reference_image {
        let image_and_metadata =
            Image::read(Path::new(&ref_path), None).expect("Could not load image");
        let image = image_and_metadata.image;
        let metadata = image_and_metadata.metadata;
        let resolution = image.resolution();
        // reference_image = Some(image);
        let mse_pixel_bounds = metadata
            .pixel_bounds
            .unwrap_or_else(|| Bounds2i::from_points(Point2i::new(0, 0), resolution));

        if !mse_pixel_bounds.overlaps(&pixel_bounds) {
            panic!("Output pixel bounds dont fit inside the reference image");
        }

        let crop_p_min = Point2i::from(pixel_bounds.p_min - mse_pixel_bounds.p_min);
        let crop_p_max = Point2i::from(pixel_bounds.p_max - mse_pixel_bounds.p_min);

        let crop_bounds = Bounds2i::from_points(crop_p_min, crop_p_max);

        let cropped_image = image.crop(crop_bounds).clone();
        let cropped_resolution = cropped_image.resolution();

        let expected_res = Point2i::new(
            pixel_bounds.p_max.x() - pixel_bounds.p_min.x(),
            pixel_bounds.p_max.y() - pixel_bounds.p_min.y(),
        );

        reference_image = Some(cropped_image);

        assert_eq!(
            cropped_resolution, expected_res,
            "Cropped reference image resolution mismatch"
        );

        if let Some(out_path) = &options.mse_reference_output {
            mse_out_file = Some(
                std::fs::File::create(out_path)
                    .expect(&format!("Failed to create MSE output file: {}", out_path)),
            );
        }
    }

    let tiles = generate_tiles(pixel_bounds);
    while wave_start < spp {
        tiles.par_iter().for_each(|tile_bounds| {
            let mut sampler = sampler_prototype.clone();

            for p_pixel in tile_bounds {
                for sample_index in wave_start..wave_end {
                    sampler.start_pixel_sample(*p_pixel, sample_index, None);
                    evaluate_pixel_sample(
                        integrator,
                        camera,
                        &mut sampler,
                        *p_pixel,
                        sample_index.try_into().unwrap(),
                        &arena,
                    );
                }
            }

            let work_done = (tile_bounds.area() as u64) * ((wave_end - wave_start) as u64);
            progress.update(work_done);
        });

        wave_start = wave_end;
        wave_end = (wave_end + next_wave_size).min(spp);

        if reference_image.is_none() {
            next_wave_size = (2 * next_wave_size).min(64);
        }

        if wave_start == spp {
            progress.done();
        }

        if wave_start == spp || options.write_partial_images || reference_image.is_some() {
            let mut metadata = ImageMetadata {
                render_time_seconds: Some(progress.elapsed_seconds()),
                samples_per_pixel: Some(wave_start as i32),
                ..Default::default()
            };

            if wave_start == spp || options.write_partial_images {
                camera.init_metadata(&mut metadata);

                if let Some(out_path) = &options.mse_reference_output {
                    camera
                        .get_film()
                        .write_image(&metadata, 1.0 / wave_start as Float, out_path);
                }
            }

            if let Some(ref_img) = &reference_image {
                let splat_scale = 1.0 / (wave_start as Float);

                let film_metadata = ImageMetadata::default();
                let film = camera.get_film();
                let film_image = film.get_image(&film_metadata, splat_scale);

                let (mse_values, _mse_debug_img) =
                    film_image.mse(&film_image.all_channels_desc(), ref_img, false);
                let mse_avg = mse_values.average();

                if let Some(file) = &mut mse_out_file {
                    writeln!(file, "{}, {:.9}", wave_start, mse_avg).ok();
                    file.flush().ok();
                }

                metadata.mse = Some(mse_avg);
            }
        }
    }
}

pub fn evaluate_pixel_sample<T: RayIntegratorTrait>(
    integrator: &T,
    camera: &Camera,
    sampler: &mut Sampler,
    pixel: Point2i,
    _sample_index: usize,
    arena: &Arena,
) {
    let mut lu = sampler.get1d();
    if get_options().disable_wavelength_jitter {
        lu = 0.5;
    }

    let lambda = camera.get_film().sample_wavelengths(lu);
    let film = camera.get_film();
    let filter = film.get_filter();
    let camera_sample = get_camera_sample(sampler, pixel, filter);
    if let Some(mut camera_ray) = camera.generate_ray_differential(camera_sample, &lambda) {
        debug_assert!(camera_ray.ray.d.norm() > 0.999);
        debug_assert!(camera_ray.ray.d.norm() < 1.001);
        let ray_diff_scale = (sampler.samples_per_pixel() as Float).sqrt().max(0.125);
        if get_options().disable_pixel_jitter {
            camera_ray.ray.scale_differentials(ray_diff_scale);
        }

        let initialize_visible_surface = film.uses_visible_surface();
        let (mut l, visible_surface) = integrator.li(
            camera_ray.ray,
            &lambda,
            sampler,
            initialize_visible_surface,
            arena,
        );
        l *= camera_ray.weight;

        let std_spectra = get_spectra_context();
        if l.has_nans() || l.y(&lambda, &std_spectra).is_infinite() {
            l = SampledSpectrum::new(0.);
        }

        film.add_sample(
            pixel,
            l,
            &lambda,
            visible_surface.as_ref(),
            camera_sample.filter_weight,
        );
    }
}
