use std::fs;
use std::path::Path;

use serde::{Deserialize, Serialize};

const CAMPAIGN_SCENARIO_TABLE_VA: u32 = 0x0062_1cf0;
pub const CAMPAIGN_SCENARIO_COUNT: usize = 16;
const CAMPAIGN_SAVE_FORMAT_VA: u32 = 0x005d_1a78;
const CAMPAIGN_PROGRESS_CONTROL_BASE_ID: u16 = 0x0c372;
const CAMPAIGN_SELECTOR_CONTROL_BASE_ID: u16 = 0x0c382;
const CAMPAIGN_SELECTOR_CONTROL_COUNT: usize = 16;
pub const OBSERVED_CAMPAIGN_SCENARIO_NAMES: [&str; CAMPAIGN_SCENARIO_COUNT] = [
    "Go West!",
    "Germantown",
    "Central Pacific",
    "Texas Tea",
    "War Effort",
    "State of Germany",
    "Britain",
    "Crossing the Alps",
    "Third Republic",
    "Orient Express",
    "Argentina Opens Up",
    "Rhodes Unfinished",
    "Japan Trembles",
    "Greenland Growing",
    "Dutchlantis",
    "California Island",
];

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CampaignPageBand {
    pub page_index: usize,
    pub progress_start_inclusive: u8,
    pub progress_end_inclusive: Option<u8>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CampaignScenarioEntry {
    pub index: usize,
    pub pointer_va: u32,
    pub pointer_va_hex: String,
    pub name: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CampaignExeInspectionReport {
    pub image_base: u32,
    pub image_base_hex: String,
    pub campaign_scenario_table_va: u32,
    pub campaign_scenario_table_va_hex: String,
    pub campaign_scenario_count: usize,
    pub campaign_save_format_va: u32,
    pub campaign_save_format_va_hex: String,
    pub campaign_save_format_string: String,
    pub campaign_progress_control_base_id: u16,
    pub campaign_progress_control_base_id_hex: String,
    pub campaign_selector_control_base_id: u16,
    pub campaign_selector_control_base_id_hex: String,
    pub campaign_selector_control_count: usize,
    pub campaign_page_bands: Vec<CampaignPageBand>,
    pub scenarios: Vec<CampaignScenarioEntry>,
    pub notes: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct PeSection {
    virtual_address: u32,
    virtual_size: u32,
    raw_data_pointer: u32,
    raw_data_size: u32,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct PeView {
    image_base: u32,
    sections: Vec<PeSection>,
}

pub fn inspect_campaign_exe_file(
    path: &Path,
) -> Result<CampaignExeInspectionReport, Box<dyn std::error::Error>> {
    let bytes = fs::read(path)?;
    inspect_campaign_exe_bytes(&bytes)
}

pub fn inspect_campaign_exe_bytes(
    bytes: &[u8],
) -> Result<CampaignExeInspectionReport, Box<dyn std::error::Error>> {
    let view = parse_pe_view(bytes)?;

    let scenarios = (0..CAMPAIGN_SCENARIO_COUNT)
        .map(|index| {
            let pointer_va = read_u32_at_va(
                bytes,
                &view,
                CAMPAIGN_SCENARIO_TABLE_VA + (index as u32 * 4),
            )?;
            let name = read_c_string_at_va(bytes, &view, pointer_va)?;
            Ok(CampaignScenarioEntry {
                index,
                pointer_va,
                pointer_va_hex: format!("0x{pointer_va:08x}"),
                name,
            })
        })
        .collect::<Result<Vec<_>, Box<dyn std::error::Error>>>()?;

    let campaign_save_format_string = read_c_string_at_va(bytes, &view, CAMPAIGN_SAVE_FORMAT_VA)?;

    Ok(CampaignExeInspectionReport {
        image_base: view.image_base,
        image_base_hex: format!("0x{:08x}", view.image_base),
        campaign_scenario_table_va: CAMPAIGN_SCENARIO_TABLE_VA,
        campaign_scenario_table_va_hex: format!("0x{CAMPAIGN_SCENARIO_TABLE_VA:08x}"),
        campaign_scenario_count: CAMPAIGN_SCENARIO_COUNT,
        campaign_save_format_va: CAMPAIGN_SAVE_FORMAT_VA,
        campaign_save_format_va_hex: format!("0x{CAMPAIGN_SAVE_FORMAT_VA:08x}"),
        campaign_save_format_string,
        campaign_progress_control_base_id: CAMPAIGN_PROGRESS_CONTROL_BASE_ID,
        campaign_progress_control_base_id_hex: format!(
            "0x{CAMPAIGN_PROGRESS_CONTROL_BASE_ID:04x}"
        ),
        campaign_selector_control_base_id: CAMPAIGN_SELECTOR_CONTROL_BASE_ID,
        campaign_selector_control_base_id_hex: format!(
            "0x{CAMPAIGN_SELECTOR_CONTROL_BASE_ID:04x}"
        ),
        campaign_selector_control_count: CAMPAIGN_SELECTOR_CONTROL_COUNT,
        campaign_page_bands: vec![
            CampaignPageBand {
                page_index: 1,
                progress_start_inclusive: 0,
                progress_end_inclusive: Some(4),
            },
            CampaignPageBand {
                page_index: 2,
                progress_start_inclusive: 5,
                progress_end_inclusive: Some(9),
            },
            CampaignPageBand {
                page_index: 3,
                progress_start_inclusive: 10,
                progress_end_inclusive: Some(12),
            },
            CampaignPageBand {
                page_index: 4,
                progress_start_inclusive: 13,
                progress_end_inclusive: None,
            },
        ],
        scenarios,
        notes: vec![
            "Campaign.win mirrors [profile+0xc4] into control 0xc372 + progress.".to_string(),
            "Campaign.win mirrors the full sixteen-byte band [profile+0xc6..+0xd5] into controls 0xc382..0xc391.".to_string(),
            "The observed page-band thresholds come from direct RT3.exe disassembly at 0x004b8d49..0x004b8d69.".to_string(),
        ],
    })
}

fn parse_pe_view(bytes: &[u8]) -> Result<PeView, Box<dyn std::error::Error>> {
    let pe_header_offset =
        read_u32_le(bytes, 0x3c).ok_or("missing DOS e_lfanew for PE header")? as usize;
    let signature = bytes
        .get(pe_header_offset..pe_header_offset + 4)
        .ok_or("truncated PE signature")?;
    if signature != b"PE\0\0" {
        return Err("invalid PE signature".into());
    }

    let file_header_offset = pe_header_offset + 4;
    let number_of_sections =
        read_u16_le(bytes, file_header_offset + 2).ok_or("missing PE section count")? as usize;
    let size_of_optional_header =
        read_u16_le(bytes, file_header_offset + 16).ok_or("missing optional header size")? as usize;

    let optional_header_offset = file_header_offset + 20;
    let optional_magic =
        read_u16_le(bytes, optional_header_offset).ok_or("missing optional header magic")?;
    if optional_magic != 0x10b {
        return Err(format!("unsupported PE optional-header magic 0x{optional_magic:04x}").into());
    }

    let image_base =
        read_u32_le(bytes, optional_header_offset + 28).ok_or("missing PE image base")?;
    let section_table_offset = optional_header_offset + size_of_optional_header;

    let mut sections = Vec::with_capacity(number_of_sections);
    for index in 0..number_of_sections {
        let section_offset = section_table_offset + index * 40;
        let virtual_size =
            read_u32_le(bytes, section_offset + 8).ok_or("truncated section virtual size")?;
        let virtual_address =
            read_u32_le(bytes, section_offset + 12).ok_or("truncated section RVA")?;
        let raw_data_size =
            read_u32_le(bytes, section_offset + 16).ok_or("truncated section raw size")?;
        let raw_data_pointer =
            read_u32_le(bytes, section_offset + 20).ok_or("truncated section raw pointer")?;
        sections.push(PeSection {
            virtual_address,
            virtual_size,
            raw_data_pointer,
            raw_data_size,
        });
    }

    Ok(PeView {
        image_base,
        sections,
    })
}

fn read_u32_at_va(bytes: &[u8], view: &PeView, va: u32) -> Result<u32, Box<dyn std::error::Error>> {
    let file_offset = map_va_to_file_offset(view, va)?;
    read_u32_le(bytes, file_offset).ok_or_else(|| format!("truncated u32 at VA 0x{va:08x}").into())
}

fn read_c_string_at_va(
    bytes: &[u8],
    view: &PeView,
    va: u32,
) -> Result<String, Box<dyn std::error::Error>> {
    let start = map_va_to_file_offset(view, va)?;
    let slice = bytes
        .get(start..)
        .ok_or_else(|| format!("VA 0x{va:08x} mapped outside file"))?;
    let end = slice
        .iter()
        .position(|&byte| byte == 0)
        .ok_or_else(|| format!("unterminated C string at VA 0x{va:08x}"))?;
    let value = String::from_utf8(slice[..end].to_vec())?;
    Ok(value)
}

fn map_va_to_file_offset(view: &PeView, va: u32) -> Result<usize, Box<dyn std::error::Error>> {
    let rva = va
        .checked_sub(view.image_base)
        .ok_or_else(|| format!("VA 0x{va:08x} below image base 0x{:08x}", view.image_base))?;

    for section in &view.sections {
        let span = section.virtual_size.max(section.raw_data_size);
        let section_end = section
            .virtual_address
            .checked_add(span)
            .ok_or("section RVA range overflow")?;
        if rva >= section.virtual_address && rva < section_end {
            let delta = rva - section.virtual_address;
            let file_offset = section
                .raw_data_pointer
                .checked_add(delta)
                .ok_or("section file offset overflow")?;
            return Ok(file_offset as usize);
        }
    }

    Err(format!("VA 0x{va:08x} did not map into any PE section").into())
}

fn read_u16_le(bytes: &[u8], offset: usize) -> Option<u16> {
    let slice = bytes.get(offset..offset + 2)?;
    Some(u16::from_le_bytes([slice[0], slice[1]]))
}

fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
    let slice = bytes.get(offset..offset + 4)?;
    Some(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
}

#[cfg(test)]
mod tests {
    use super::{
        CAMPAIGN_SAVE_FORMAT_VA, CAMPAIGN_SCENARIO_COUNT, CAMPAIGN_SCENARIO_TABLE_VA,
        inspect_campaign_exe_bytes,
    };

    fn build_test_pe() -> Vec<u8> {
        let image_base = 0x0040_0000u32;
        let section_rva = 0x001c_0000u32;
        let section_raw = 0x0000_0200u32;
        let section_size = 0x0007_0000usize;
        let mut bytes = vec![0u8; section_raw as usize + section_size];

        bytes[0..2].copy_from_slice(b"MZ");
        bytes[0x3c..0x40].copy_from_slice(&(0x80u32).to_le_bytes());
        bytes[0x80..0x84].copy_from_slice(b"PE\0\0");

        let file_header = 0x84usize;
        bytes[file_header + 2..file_header + 4].copy_from_slice(&(1u16).to_le_bytes());
        bytes[file_header + 16..file_header + 18].copy_from_slice(&(0xe0u16).to_le_bytes());

        let optional_header = file_header + 20;
        bytes[optional_header..optional_header + 2].copy_from_slice(&(0x10bu16).to_le_bytes());
        bytes[optional_header + 28..optional_header + 32]
            .copy_from_slice(&image_base.to_le_bytes());

        let section_header = optional_header + 0xe0;
        bytes[section_header..section_header + 5].copy_from_slice(b".data");
        bytes[section_header + 8..section_header + 12]
            .copy_from_slice(&(section_size as u32).to_le_bytes());
        bytes[section_header + 12..section_header + 16].copy_from_slice(&section_rva.to_le_bytes());
        bytes[section_header + 16..section_header + 20]
            .copy_from_slice(&(section_size as u32).to_le_bytes());
        bytes[section_header + 20..section_header + 24].copy_from_slice(&section_raw.to_le_bytes());

        let scenario_table_file =
            (CAMPAIGN_SCENARIO_TABLE_VA - image_base - section_rva + section_raw) as usize;
        let format_file =
            (CAMPAIGN_SAVE_FORMAT_VA - image_base - section_rva + section_raw) as usize;

        let scenario_names = [
            "Go West!",
            "Germantown",
            "Central Pacific",
            "Texas Tea",
            "War Effort",
            "State of Germany",
            "Britain",
            "Crossing the Alps",
            "Third Republic",
            "Orient Express",
            "Argentina Opens Up",
            "Rhodes Unfinished",
            "Japan Trembles",
            "Greenland Growing",
            "Dutchlantis",
            "California Island",
        ];

        let mut string_cursor = scenario_table_file + CAMPAIGN_SCENARIO_COUNT * 4;
        for (index, name) in scenario_names.iter().enumerate() {
            let pointer_va =
                image_base + section_rva + (string_cursor - section_raw as usize) as u32;
            bytes[scenario_table_file + index * 4..scenario_table_file + (index + 1) * 4]
                .copy_from_slice(&pointer_va.to_le_bytes());
            bytes[string_cursor..string_cursor + name.len()].copy_from_slice(name.as_bytes());
            bytes[string_cursor + name.len()] = 0;
            string_cursor += name.len() + 1;
        }

        let format = b"%s%02d.gmc\0";
        bytes[format_file..format_file + format.len()].copy_from_slice(format);

        bytes
    }

    #[test]
    fn inspects_campaign_exe_tables_from_synthetic_pe() {
        let bytes = build_test_pe();
        let report = inspect_campaign_exe_bytes(&bytes).expect("campaign exe inspection");

        assert_eq!(report.campaign_scenario_count, 16);
        assert_eq!(report.campaign_save_format_string, "%s%02d.gmc");
        assert_eq!(
            report.scenarios.first().map(|entry| entry.name.as_str()),
            Some("Go West!")
        );
        assert_eq!(
            report.scenarios.last().map(|entry| entry.name.as_str()),
            Some("California Island")
        );
        assert_eq!(report.campaign_page_bands.len(), 4);
        assert_eq!(report.campaign_page_bands[1].progress_start_inclusive, 5);
        assert_eq!(
            report.campaign_page_bands[1].progress_end_inclusive,
            Some(9)
        );
    }
}
