use std::{
    collections::VecDeque,
    io::Cursor,
    sync::Arc,
};

use image::{DynamicImage, RgbaImage};
use pdf::{
    file::FileOptions,
    object::{ImageXObject, ObjectWrite, PlainRef, Resolve, Stream},
    primitive::{PdfStream, Primitive},
};
use pdf::object::Resources;
use pdf_render::{load_image, BlendMode};
use crate::{CosFile, PathTrace, Step};

#[macro_export]
macro_rules! t {
    ($result:expr) => {{
        match $result {
            Ok(f) => f,
            Err(e) => return Err(e.to_string()),
        }
    }};
}

pub fn get_prim_by_steps_with_file(
    mut steps: VecDeque<Step>,
    file: &CosFile,
) -> Result<(Step, Primitive, Vec<PathTrace>), String> {
    if steps.len() == 0 {
        return Err(String::from(format!("{:?} is not a valid path!", steps)));
    }
    let step = steps.pop_front().unwrap();
    let (mut parent, trace) = resolve_parent(step.clone(), file)?;

    let mut last_jump: Option<PlainRef> = trace.last_ref.clone();
    let mut trace = vec![trace];

    let mut current_prim = &parent;
    while !steps.is_empty() {
        let step = steps.pop_front().unwrap();

        current_prim = resolve_step(&current_prim, &step)?;
        if let Primitive::Reference(xref) = current_prim {
            last_jump = Some(xref.clone());
            parent = resolve_p_ref(xref.clone(), file)?;
            current_prim = &parent;
        }
        trace.push(PathTrace::new(step.get_key(), last_jump.clone()));
    }
    Ok((step, current_prim.clone(), trace))
}

pub fn resolve_parent(step: Step, file: &CosFile) -> Result<(Primitive, PathTrace), String> {
    match step {
        Step::Page(page_num) => retrieve_page(page_num, file),
        Step::Number(obj_num) => {
            let parent = resolve_xref(obj_num, file)?;
            Ok((parent, PathTrace::new(step.get_key(), Some(PlainRef {id: obj_num, gen: 0}))))
        },
        Step::Trailer => {
            let parent = retrieve_trailer(file);
            Ok((parent, PathTrace::trailer()))
        },
        _ => Err(String::from(format!("{:?} is not a valid path!", step))),
    }
}

pub fn resolve_step<'a>(current_prim: &'a Primitive, step: &Step) -> Result<&'a Primitive, String> {
    Ok(match step {
        Step::Number(index) => match current_prim {
            Primitive::Array(prim_array) => {
                let i = index.clone() as usize;
                if prim_array.len() <= i {
                    return Err(String::from(format!(
                        "{} index out of bounds!",
                        step.get_key()
                    )));
                }
                &prim_array[i]
            }
            p => {
                return Err(String::from(format!(
                    "{} is not indexed with numbers!",
                    p.get_debug_name()
                )))
            }
        },
        Step::String(key) => match current_prim {
            Primitive::Dictionary(dict) => match dict.get(key) {
                Some(prim) => prim,
                None => {
                    return Err(String::from(format!(
                        "Key {} does not exist in Dictionary!",
                        key
                    )))
                }
            },
            Primitive::Stream(stream) => match stream.info.get(key) {
                Some(prim) => prim,
                None => {
                    return Err(String::from(format!(
                        "Key {} does not exist in Info Dictionary!",
                        key
                    )))
                }
            },
            p => {
                return Err(String::from(format!(
                    "{} has no String paths!",
                    p.get_debug_name()
                )))
            }
        },
        _ => return Err(format!("Invalid Step: {}", step.get_key())),
    })
}

pub fn retrieve_trailer(file: &CosFile) -> Primitive {
    let mut updater = FileOptions::uncached().storage();
    file.trailer.to_primitive(&mut updater).unwrap()
}

pub fn retrieve_page(page_num: u32, file: &CosFile) -> Result<(Primitive, PathTrace), String> {
    if page_num <= 0 {
        return Err("Page 0 does not exist, use 1-based index!".to_string());
    }
    let page_rc = t!(file.get_page(page_num - 1));
    let p_ref = page_rc.get_ref().get_inner();
    Ok((
        resolve_p_ref(p_ref, file)?,
        PathTrace::new(format!("Page{}", page_num), Some(p_ref)),
    ))
}

pub fn resolve_xref(id: u64, file: &CosFile) -> Result<Primitive, String> {
    let plain_ref = PlainRef { id, gen: 0 };
    resolve_p_ref(plain_ref, file)
}

pub fn resolve_p_ref(plain_ref: PlainRef, file: &CosFile) -> Result<Primitive, String> {
    file.resolver()
        .resolve(plain_ref)
        .map_err(|e| e.to_string())
}

pub fn get_prim_by_path_with_file(
    path: &str,
    file: &CosFile,
) -> Result<(Step, Primitive, Vec<PathTrace>), String> {
    get_prim_by_steps_with_file(Step::parse(path), file)
}

pub fn get_image_by_path(path: &str, file: &CosFile) -> Result<DynamicImage, String> {
    let stream = get_pdf_stream_by_path_with_file(path, file)?;
    let resolver = file.resolver();
    let img = t!(ImageXObject::from_stream(stream, &resolver));
    let img_data = t!(load_image(&img, &Resources::default(), &resolver, BlendMode::Overlay));
    let h = img_data.height();
    let w = img_data.width();
    let mut data: Vec<u8> = Vec::with_capacity((h * w * 4) as usize);
    for x in img_data.rgba_data() {
        data.push(*x);
    }
    let rgba_img = RgbaImage::from_raw(w, h, data).expect("Image conversion failed");
    Ok(DynamicImage::ImageRgba8(rgba_img))
}

fn parse_image_lossy(data: Arc<[u8]>) -> Result<DynamicImage, String> {
    let img = image::ImageReader::new(Cursor::new(data));
    match img.decode() {
        Ok(img) => Ok(img),
        Err(e) => Err(format!("Failed to decode image without color space: {}", e)),
    }
}

pub fn get_pdf_stream_by_path_with_file(path: &str, file: &CosFile) -> Result<PdfStream, String> {
    let mut steps = Step::parse(path);
    if steps
        .pop_back()
        .filter(|last| *last == Step::Data)
        .is_none()
    {
        return Err(format!("Path {} does not end with Data", path));
    }
    let (_, prim, _) = get_prim_by_steps_with_file(steps, file)?;

    let Primitive::Stream(stream) = prim else {
        return Err(format!("Path {} does not point to a stream", path));
    };
    Ok(stream)
}

pub fn get_stream_data_by_path_with_file(path: &str, file: &CosFile) -> Result<Vec<u8>, String> {
    let stream = get_pdf_stream_by_path_with_file(path, file)?;
    let data =
        t!(t!(Stream::<Primitive>::from_stream(stream, &file.resolver())).data(&file.resolver()));
    Ok(data.to_vec())
}
