use std::{env, fs::create_dir_all};

use opencv::{
    core::{AccessFlag, CV_8U, Mat, Point, Rect2i, Scalar, UMat, UMatTraitConst, Vector},
    imgcodecs::imwrite_def,
    imgproc::{COLOR_GRAY2RGB, LINE_8, cvt_color, cvt_color_def, draw_contours, rectangle_def},
};

use crate::models;

use models::{
    LcdScreenCandidate, ProcessingError, ReadingIdentificationError, RejectedLcdScreenCandidate,
};

/**  
 * Named unsafe as if the same debugger instance is used across different executions of BloodPressureReadingExtractor
 * then the files will be overwritten with new values and if it is used across concurrent executions then the files may
 * be corrupted or an error may be thrown. A new BloodPressureReadingExtractor should be constructed for each run when
 * using this debugger.
 * 
 * This debugger writes the debug image files to the system temporary folder. The folder lives inside bpm-ocr/${folder_name}
*/
pub struct UnsafeTempFolderDebugger {
    folder_name: String,
    debug_enabled: bool,
}

pub struct NoDebug {}

pub trait BpmOcrDebugOutputter {
    fn new(unique_session_name: &str, debug_enabled: bool) -> Self;
    fn output(&self, image: &Mat, stage_description: &str) -> Result<(), ProcessingError>;
    fn debug_enabled(&self) -> bool;

    fn debug_after_canny(&self, image: &UMat) -> Result<(), ProcessingError> {
        if !self.debug_enabled() {
            return Ok(());
        }

        let converted_to_mat = image.get_mat(AccessFlag::ACCESS_READ)?;

        self.output(&converted_to_mat, "after_canny")
    }

    fn debug_lcd_contour_candidates(
        &self,
        image: &Mat,
        candidates: &Vec<LcdScreenCandidate>,
        rejections: Vec<RejectedLcdScreenCandidate>,
    ) -> Result<(), ProcessingError> {
        if !self.debug_enabled() {
            return Ok(());
        }

        let mut colour: Mat = Mat::default();

        cvt_color(&image, &mut colour, COLOR_GRAY2RGB, 0)?;

        for rejection in rejections {
            let mut x: Vector<Vector<Point>> = Vector::new();
            x.push(rejection.contour);

            draw_contours(
                &mut colour,
                &x,
                0,
                Scalar::new(255.0, 0.0, 0.0, 0.1),
                1,
                LINE_8.into(),
                &Mat::default(),
                i32::MAX,
                Point::default(),
            )?;
        }

        for candidate in candidates {
            let mut x: Vector<Vector<Point>> = Vector::new();
            x.push(candidate.contour.clone());

            draw_contours(
                &mut colour,
                &x,
                0,
                Scalar::new(0., 255.0, 0.0, 0.1),
                1,
                LINE_8.into(),
                &Mat::default(),
                i32::MAX,
                Point::default(),
            )?;
        }

        self.output(&colour, "contour_candidates")
    }

    fn debug_after_perspective_transform(&self, image: &Mat) -> Result<(), ProcessingError> {
        if !self.debug_enabled() {
            return Ok(());
        }

        self.output(&image, "after_perspective_transform")
    }

    fn debug_digits_before_morph(&self, image: &Mat) -> Result<(), ProcessingError> {
        if !self.debug_enabled() {
            return Ok(());
        }

        self.output(image, "digits_before_morph")
    }

    fn debug_digits_after_dilation(&self, image: &Mat) -> Result<(), ProcessingError> {
        if !self.debug_enabled() {
            return Ok(());
        }

        self.output(&image, "digits_after_dilation")
    }

    fn debug_digit_locations(
        &self,
        image: &Mat,
        digit_locations: &Vec<Rect2i>,
    ) -> Result<(), ProcessingError> {
        if !self.debug_enabled() {
            return Ok(());
        }

        let mut temp_image = Mat::default();
        cvt_color_def(&image, &mut temp_image, CV_8U)?;

        for b in digit_locations {
            rectangle_def(&mut temp_image, *b, Scalar::new(0.0, 255.0, 0.0, 0.0))?;
        }

        self.output(&temp_image, "digit_locations")
    }
}

impl UnsafeTempFolderDebugger {
    pub fn using_timestamp_folder_name(debug_enabled: bool) -> Self {
        let now = chrono::offset::Local::now();
        let folder_name: String = now.format("%Y-%m-%d-%H-%M-%S").to_string();

        UnsafeTempFolderDebugger {
            folder_name,
            debug_enabled,
        }
    }
}

impl BpmOcrDebugOutputter for UnsafeTempFolderDebugger {
    fn new(unique_session_name: &str, debug_enabled: bool) -> Self {
        UnsafeTempFolderDebugger {
            folder_name: unique_session_name.to_string(),
            debug_enabled: debug_enabled,
        }
    }

    fn output(&self, image: &Mat, stage_description: &str) -> Result<(), ProcessingError> {
        let folder_path = env::temp_dir().join("bmp-ocr").join(&self.folder_name);

        // TODO: create at construction
        create_dir_all(&folder_path).map_err(|_| {
            ProcessingError::AppError(ReadingIdentificationError::InternalError(
                "Could not create a temporary folder for debugging image processing",
            ))
        })?;

        let file_name = format!("{}.jpeg", &stage_description);

        let binding = folder_path.join(file_name);

        let file_path = binding.to_str().ok_or_else(|| {
            ProcessingError::AppError(ReadingIdentificationError::InternalError(
                &"Could not create a temporary folder for debugging image processing",
            ))
        })?;

        imwrite_def(&file_path, &image)?;
        Ok(())
    }

    fn debug_enabled(&self) -> bool {
        self.debug_enabled
    }
}

impl BpmOcrDebugOutputter for NoDebug {
    fn new(_: &str, _: bool) -> Self {
        NoDebug {  }
    }

    fn output(&self, _: &Mat, _: &str) -> Result<(), ProcessingError> {
        Ok(())
    }

    fn debug_enabled(&self) -> bool {
        false
    }
}