use assembly_core::nom::{
    bytes::complete::take,
    combinator::{cond, map, map_res},
    multi::length_count,
    number::complete::{le_u32, le_u64, le_u8},
    IResult,
};
use std::convert::TryFrom;
use super::core::{
    FileVersion, SceneRef, SceneTransition, SceneTransitionInfo, SceneTransitionPoint, ZoneFile,
};
use assembly_core::nom_ext::{count_2, count_5};
use assembly_core::parser::{parse_quat, parse_u8_string, parse_vec3f, parse_world_id};
use assembly_core::types::Placement3D;
pub fn parse_file_version(input: &[u8]) -> IResult<&[u8], FileVersion> {
    map(le_u32, FileVersion::from)(input)
}
pub fn parse_file_revision(input: &[u8], version: FileVersion) -> IResult<&[u8], Option<u32>> {
    cond(version.id() >= 0x24, le_u32)(input)
}
pub fn parse_spawn_point<'a>(
    input: &'a [u8],
    version: FileVersion,
) -> IResult<&'a [u8], Option<Placement3D>> {
    let inner = |i: &'a [u8]| {
        let (i, a) = parse_vec3f(i)?;
        let (i, b) = parse_quat(i)?;
        Ok((i, Placement3D { pos: a, rot: b }))
    };
    cond(version.id() >= 0x26, inner)(input)
}
pub fn parse_scene_count(version: FileVersion) -> fn(input: &[u8]) -> IResult<&[u8], usize> {
    fn pre_x25(input: &[u8]) -> IResult<&[u8], usize> {
        map_res(le_u8, usize::try_from)(input)
    }
    fn post_x25(input: &[u8]) -> IResult<&[u8], usize> {
        map_res(le_u32, usize::try_from)(input)
    }
    if version.id() >= 0x25 {
        post_x25
    } else {
        pre_x25
    }
}
fn parse_scene_ref(input: &[u8]) -> IResult<&[u8], SceneRef> {
    let (input, file_name) = parse_u8_string(input)?;
    let (input, id) = le_u32(input)?;
    let (input, layer) = le_u32(input)?;
    let (input, name) = parse_u8_string(input)?;
    let (input, _) = take(3usize)(input)?;
    Ok((
        input,
        SceneRef {
            file_name,
            id,
            layer,
            name,
        },
    ))
}
fn parse_scene_transition_point(input: &[u8]) -> IResult<&[u8], SceneTransitionPoint> {
    let (input, a) = le_u64(input)?;
    let (input, b) = parse_vec3f(input)?;
    Ok((
        input,
        SceneTransitionPoint {
            scene_id: a,
            point: b,
        },
    ))
}
fn parse_scene_transition_info(
    version: FileVersion,
) -> fn(&[u8]) -> IResult<&[u8], SceneTransitionInfo> {
    fn x22_to_x26(i: &[u8]) -> IResult<&[u8], SceneTransitionInfo> {
        map(
            count_5(parse_scene_transition_point),
            SceneTransitionInfo::from,
        )(i)
    }
    fn post_x27(i: &[u8]) -> IResult<&[u8], SceneTransitionInfo> {
        map(
            count_2(parse_scene_transition_point),
            SceneTransitionInfo::from,
        )(i)
    }
    if version.id() <= 0x21 || version.id() >= 0x27 {
        post_x27
    } else {
        x22_to_x26
    }
}
fn parse_scene_transition(
    version: FileVersion,
) -> impl Fn(&[u8]) -> IResult<&[u8], SceneTransition> + Copy {
    let sti_parser = parse_scene_transition_info(version);
    move |i: &[u8]| {
        let (i, name) = cond(version.id() < 0x25, parse_u8_string)(i)?;
        let (i, points) = sti_parser(i)?;
        Ok((i, SceneTransition { name, points }))
    }
}
fn parse_scene_transitions(
    version: FileVersion,
) -> impl Fn(&[u8]) -> IResult<&[u8], Option<Vec<SceneTransition>>> {
    let st_parser = parse_scene_transition(version);
    move |i: &[u8]| cond(version.id() >= 0x20, length_count(le_u32, st_parser))(i)
}
#[allow(clippy::many_single_char_names)]
pub fn parse_zone_file(input: &[u8]) -> IResult<&[u8], ZoneFile<Vec<u8>>> {
    let (input, file_version) = parse_file_version(input)?;
    let sc_parser = parse_scene_count(file_version);
    let st_parser = parse_scene_transitions(file_version);
    let (input, file_revision) = parse_file_revision(input, file_version)?;
    let (input, world_id) = parse_world_id(input)?;
    let (input, spawn_point) = parse_spawn_point(input, file_version)?;
    let (input, scene_refs) = length_count(sc_parser, parse_scene_ref)(input)?;
    let (input, g) = parse_u8_string(input)?;
    let (input, map_filename) = parse_u8_string(input)?;
    let (input, map_name) = parse_u8_string(input)?;
    let (input, map_description) = parse_u8_string(input)?;
    let (input, scene_transitions) = st_parser(input)?;
    let (input, path_data) = cond(file_version.min(0x23), length_count(le_u32, le_u8))(input)?;
    Ok((
        input,
        ZoneFile {
            file_version,
            file_revision,
            world_id,
            spawn_point,
            scene_refs,
            something: g,
            map_filename,
            map_name,
            map_description,
            scene_transitions,
            path_data,
        },
    ))
}
#[test]
fn test_parse() {
    use assembly_core::nom::error::ErrorKind;
    assert_eq!(
        parse_file_revision(&[20, 0, 0, 0], FileVersion::from(0x24)),
        Ok((&[][..], Some(20)))
    );
    assert_eq!(
        parse_file_revision(&[20, 0, 0, 0], FileVersion::from(0x23)),
        Ok((&[20, 0, 0, 0][..], None))
    );
    assert_eq!(
        parse_scene_count(FileVersion::from(0x24))(&[20]),
        Ok((&[][..], 20))
    );
    assert_eq!(
        parse_scene_count(FileVersion::from(0x25))(&[20, 0, 0, 0]),
        Ok((&[][..], 20))
    );
    assert_eq!(
        parse_u8_string::<(&[u8], ErrorKind)>(&[2, 65, 66]),
        Ok((&[][..], String::from("AB")))
    );
}