use super::*;
use crate::dump_utils::indent;
use anyhow::bail;
use ms_pdb::codeview::arch::{Arch, ArchReg};
use ms_pdb::syms::SymData;
use ms_pdb::tpi::TypeStream;
use ms_pdb::types::ItemId;
use tracing::warn;

pub fn dump_sym(
    out: &mut String,
    context: &mut DumpSymsContext<'_>,
    record_offset: u32,
    kind: SymKind,
    data: &[u8],
) -> anyhow::Result<()> {
    fn item_ref(
        out: &mut dyn std::fmt::Write,
        context: &super::sym::DumpSymsContext,
        item: ItemId,
    ) {
        match super::types::dump_item_short(out, context, item) {
            Ok(()) => {}
            Err(e) => {
                _ = write!(out, "(error: {e:?}");
            }
        }
    }

    fn ty_ref(
        out: &mut dyn std::fmt::Write,
        context: &super::sym::DumpSymsContext,
        type_index: TypeIndex,
    ) {
        match super::types::dump_type_index_short(out, context, type_index) {
            Ok(()) => {}
            Err(e) => {
                _ = write!(out, "(error: {e:?}");
            }
        }
    }

    if context.scope_depth == 0 && kind.starts_scope() {
        writeln!(out)?;
    }

    if context.show_record_offsets {
        write!(out, "{record_offset:08x} : ")?;
    }

    if context.scope_depth > 0 {
        write!(out, "{}", indent(context.scope_depth * 2))?;
    }

    write!(out, "{kind:?}: ")?;

    match SymData::parse(kind, data)? {
        // TODO: S_CALLEES is using a different kind of TypeIndex value.
        // Suppress these for now.
        _ if kind == SymKind::S_CALLEES || kind == SymKind::S_INLINESITE => {
            write!(out, "<unavailable>")?;
        }

        SymData::Pub(pub_data) => {
            write!(
                out,
                "{}, flags: {:08x}, {}",
                pub_data.fixed.offset_segment,
                pub_data.fixed.flags.get(),
                pub_data.name
            )?;
        }

        SymData::Udt(udt_data) => {
            ty_ref(out, context, udt_data.type_);
            write!(out, " {}", udt_data.name)?;
        }

        SymData::Constant(constant_data) => {
            ty_ref(out, context, constant_data.type_);
            write!(out, " {} = {}", constant_data.name, constant_data.value)?;
        }

        SymData::ManagedConstant(constant_data) => {
            write!(out, "Token 0x{:x}", constant_data.token)?;
            write!(out, " {} = {}", constant_data.name, constant_data.value)?;
        }

        SymData::RefSym2(sym_ref) => {
            write!(
                out,
                "({}, {:08x}) {}",
                sym_ref.header.module_index.get(),
                sym_ref.header.symbol_offset.get(),
                sym_ref.name
            )?;
        }

        SymData::Data(data) => {
            write!(out, "{} ", data.header.offset_segment,)?;
            ty_ref(out, context, data.header.type_.get());
            write!(out, " {}", data.name)?;
        }

        SymData::ThreadData(thread_storage) => {
            write!(
                out,
                "{}, Type: 0x{:04X}, {}",
                thread_storage.header.offset_segment,
                thread_storage.header.type_.0,
                thread_storage.name
            )?;
        }

        SymData::ObjName(obj_name) => {
            write!(out, "sig: 0x{:08x} {}", obj_name.signature, obj_name.name)?;
        }

        SymData::Compile3(compile3) => {
            write!(out, "{}", compile3.name)?;
        }

        SymData::Proc(proc) => {
            write!(
                out,
                "{} ..+ 0x{:x}, ",
                proc.fixed.offset_segment, proc.fixed.proc_len
            )?;
            ty_ref(out, context, proc.fixed.proc_type.get());
            write!(out, " {}", proc.name)?;
        }

        SymData::ManagedProc(proc) => {
            write!(out, "Token 0x{:x} {}", proc.fixed.token.get(), proc.name)?;
        }

        SymData::End => {}

        SymData::Unknown => {
            write!(out, "Unknown")?;
        }

        SymData::Annotation(ann) => {
            writeln!(out, "{}", ann.fixed.offset)?;
            for s in ann.iter_strings() {
                writeln!(out, "    {s}")?;
            }
        }

        SymData::FrameProc(_) => {}

        SymData::RegRel(reg_rel) => {
            let reg = reg_rel.fixed.register.get();
            let arch_reg = ArchReg::new(context.arch, reg);
            write!(
                out,
                "{arch_reg} + 0x{offset:x}, ",
                // reg_rel.fixed.register.get(),
                offset = reg_rel.fixed.offset.get()
            )?;
            ty_ref(out, context, reg_rel.fixed.ty.get());
            write!(out, " {}", reg_rel.name)?;
        }

        SymData::Block(block) => {
            write!(out, "length: 0x{:x}", block.fixed.length.get())?;

            if !block.name.is_empty() {
                write!(out, " name: {}", block.name)?;
            }
        }

        SymData::Local(local) => {
            ty_ref(out, context, local.fixed.ty.get());
            write!(out, " {}", local.name)?;
        }

        SymData::DefRangeFramePointerRel(def_range) => {
            write!(
                out,
                "bp+ 0x{:x}, {} ..+ 0x{:x}",
                def_range.fixed.offset_to_frame_pointer,
                def_range.fixed.range.start,
                def_range.fixed.range.range_size.get()
            )?;
            if !def_range.gaps.is_empty() {
                write!(out, ", num_gaps: {}", def_range.gaps.len())?;
            }
        }

        SymData::Trampoline(_) => {}

        SymData::UsingNamespace(ns) => {
            write!(out, "using {}", ns.namespace)?;
        }

        SymData::BuildInfo(b) => {
            item_ref(out, context, b.item);
        }

        SymData::InlineSite(site) => {
            item_ref(out, context, site.fixed.inlinee.get());
        }

        SymData::InlineSite2(site) => {
            item_ref(out, context, site.fixed.inlinee.get());
        }

        SymData::InlineSiteEnd => {}

        SymData::DefRangeRegister(r) => {
            let reg = ArchReg::new(context.arch, r.fixed.reg.get());
            write!(out, "register: {reg}")?;
        }

        SymData::DefRangeRegisterRel(r) => {
            write!(
                out,
                "base register: 0x{:x}, base pointer offset: {}",
                r.fixed.base_reg, r.fixed.base_pointer_offset
            )?;
        }

        SymData::DefRangeSubFieldRegister(_) => {}

        SymData::DefRangeFramePointerRelFullScope(r) => {
            write!(out, "frame pointer offset: {}", r.frame_pointer_offset)?;
        }

        SymData::Label(label) => {
            write!(out, "{} : {}", label.fixed.offset_segment, label.name)?;
        }

        SymData::FunctionList(funcs) => {
            if !funcs.funcs.is_empty() {
                writeln!(out)?;
                for f in funcs.funcs.iter() {
                    item_ref(out, context, f.get());
                    writeln!(out)?;
                }
            }
        }

        SymData::FrameCookie(_) => {}

        SymData::CallSiteInfo(site) => {
            write!(out, "{} ", site.offset)?;
            ty_ref(out, context, site.func_type.get());
        }

        SymData::HeapAllocSite(site) => {
            write!(out, "{} ", site.offset)?;
            ty_ref(out, context, site.func_type.get());
        }

        SymData::HotPatchFunc(hp) => {
            write!(out, "0x{:x} : {}", hp.func, hp.name)?;
        }

        SymData::CoffGroup(group) => {
            write!(
                out,
                "{} : {} + {}",
                group.name,
                group.fixed.off_seg,
                group.fixed.cb.get()
            )?;
        }

        SymData::ArmSwitchTable(table) => {
            writeln!(out)?;
            writeln!(
                out,
                "    switch_type:                 {:?}",
                table.switch_type()
            )?;
            writeln!(
                out,
                "    num_entries:                 {}",
                table.num_entries
            )?;
            writeln!(out, "    base location:               {}", table.base())?;
            writeln!(out, "    branch instruction location: {}", table.branch())?;
            writeln!(out, "    jump table location:         {}", table.table())?;
        }

        SymData::Section(section) => {
            write!(
                out,
                "section {:4}, rva {:#08x}, size {:#08x} : {:?}",
                section.fixed.section.get(),
                section.fixed.rva.get(),
                section.fixed.cb.get(),
                section.name
            )?;
        }
    }

    writeln!(out)?;

    if kind.starts_scope() {
        context.scope_depth += 1;
    }

    if kind.ends_scope() {
        if context.scope_depth > 0 {
            context.scope_depth -= 1;
        } else {
            warn!("scope depth is mismatched");
        }
    }

    Ok(())
}

pub struct DumpSymsContext<'a> {
    pub scope_depth: u32,
    pub type_stream: &'a TypeStream<Vec<u8>>,
    pub show_record_offsets: bool,
    pub show_type_index: bool,
    pub ipi: &'a TypeStream<Vec<u8>>,
    pub arch: Arch,
}

impl<'a> DumpSymsContext<'a> {
    pub fn new(
        arch: Arch,
        type_stream: &'a TypeStream<Vec<u8>>,
        ipi: &'a TypeStream<Vec<u8>>,
    ) -> Self {
        Self {
            scope_depth: 0,
            type_stream,
            show_record_offsets: true,
            show_type_index: false,
            ipi,
            arch,
        }
    }
}

pub fn dump_globals(
    p: &Pdb,
    skip_opt: Option<usize>,
    max_opt: Option<usize>,
    show_bytes: bool,
    show_types: bool,
) -> anyhow::Result<()> {
    println!("Global symbols:");
    let arch = p.arch()?;
    let gss = p.gss()?;
    let tpi = p.read_type_stream()?;
    let ipi = p.read_ipi_stream()?;
    dump_symbol_stream(
        arch,
        &tpi,
        &ipi,
        &gss.stream_data,
        skip_opt,
        max_opt,
        0,
        show_bytes,
        show_types,
    )?;
    Ok(())
}

pub fn dump_symbol_stream(
    arch: Arch,
    type_stream: &TypeStream<Vec<u8>>,
    ipi: &TypeStream<Vec<u8>>,
    symbol_records: &[u8],
    skip_opt: Option<usize>,
    max_opt: Option<usize>,
    stream_offset: u32,
    show_bytes: bool,
    show_types: bool,
) -> anyhow::Result<()> {
    let mut iter = SymIter::new(symbol_records).with_ranges();

    // We have to manually decode all the records that we are skipping;
    // there is no index structure.
    if let Some(skip) = skip_opt {
        for _ in 0..skip {
            if iter.next().is_none() {
                break;
            }
        }
    }

    let mut num_found = 0;
    let mut out = String::new();
    let mut context = DumpSymsContext::new(arch, type_stream, ipi);
    context.show_type_index = show_types;

    for (record_range, sym) in iter {
        out.clear();

        dump_sym(
            &mut out,
            &mut context,
            stream_offset + record_range.start as u32,
            sym.kind,
            sym.data,
        )?;
        print!("{out}");

        if show_bytes {
            let record_bytes = &symbol_records[record_range.clone()];
            println!(
                "{:?}",
                HexDump::new(record_bytes).at(stream_offset as usize + record_range.start)
            );
        }

        num_found += 1;
        if let Some(max) = max_opt {
            if num_found >= max {
                break;
            }
        }
    }

    Ok(())
}

/// Displays module symbols, for all modules or for a specific module.
#[derive(clap::Parser, Debug)]
pub struct DumpModuleSymbols {
    /// The module to dump
    pub module_index: Option<u32>,

    /// Skip this many symbol records before beginning the dump.
    #[arg(long)]
    pub skip: Option<usize>,

    /// Stop after this many symbol records have been displayed.
    #[arg(long)]
    pub max: Option<usize>,

    /// Dump the hex bytes of each symbol record.
    #[arg(long)]
    pub bytes: bool,

    /// Show the contents of the Global Refs section.
    #[arg(long)]
    pub global_refs: bool,
}

pub fn dump_module_symbols(pdb: &Pdb, options: DumpModuleSymbols) -> anyhow::Result<()> {
    let dbi = pdb.read_dbi_stream()?;

    let mut found_wanted_module = false;

    let tpi = pdb.read_type_stream()?;
    let ipi = pdb.read_ipi_stream()?;

    println!("Types:");
    println!("    type_index_begin: {:?}", tpi.type_index_begin());
    println!("    type_index_end:   {:?}", tpi.type_index_end());
    println!(
        "    num_types:        {:8}",
        tpi.type_index_end().0 - tpi.type_index_begin().0
    );
    println!("    num_starts:       {:8}", tpi.record_starts().len());

    println!();

    for (module_index, module) in dbi.iter_modules().enumerate() {
        if let Some(wanted_module_index) = options.module_index {
            if wanted_module_index as usize == module_index {
                found_wanted_module = true;
            } else {
                continue;
            }
        }

        println!("Module #{module_index}");
        println!("-------------------");
        println!();

        let Some(module_stream) = pdb.read_module_stream(&module)? else {
            println!("Module does not have a module stream (no symbols for module)");
            continue;
        };

        let arch = pdb.arch()?;

        dump_symbol_stream(
            arch,
            &tpi,
            &ipi,
            module_stream.sym_data()?,
            options.skip,
            options.max,
            4,
            options.bytes,
            false,
        )?;

        println!();

        if options.global_refs {
            println!("Global Refs");
            println!("-----------");
            println!();

            let module_global_refs = module_stream.global_refs()?;
            if !module_global_refs.is_empty() {
                let gss = pdb.gss()?;

                let mut out = String::new();
                let mut context = DumpSymsContext::new(arch, &tpi, &ipi);

                for &global_ref in module_global_refs.iter() {
                    let global_ref = global_ref.get();
                    // global_ref is an index into the GSS

                    let global_sym = gss.get_sym_at(global_ref)?;
                    dump_sym(
                        &mut out,
                        &mut context,
                        global_ref,
                        global_sym.kind,
                        global_sym.data,
                    )?;
                    print!("{out}");
                }
            } else {
                println!("(none)");
            }
        }

        if found_wanted_module {
            break;
        }
    }

    if let Some(wanted_module_index) = options.module_index {
        if !found_wanted_module {
            bail!(
                "Could not find a module with index #{}",
                wanted_module_index
            );
        }
    }

    Ok(())
}

pub fn dump_gsi(p: &Pdb) -> Result<()> {
    let arch = p.arch()?;
    let gsi = p.gsi()?;
    let gss = p.gss()?;
    let tpi = p.read_type_stream()?;
    let ipi = p.read_ipi_stream()?;

    println!("*** GLOBALS");
    println!();

    let mut context = DumpSymsContext::new(arch, &tpi, &ipi);

    let mut out = String::new();
    for sym in gsi.names().iter(gss) {
        out.clear();
        // TODO: show the correct record offset, instead of 0
        dump_sym(&mut out, &mut context, 0, sym.kind, sym.data)?;
        println!("{out}");
    }

    println!();

    Ok(())
}

pub fn dump_psi(p: &Pdb) -> Result<()> {
    let arch = p.arch()?;
    let psi = p.read_psi()?;
    let gss = p.gss()?;
    let tpi = p.read_type_stream()?;
    let ipi = p.read_ipi_stream()?;

    println!("*** PUBLICS");
    println!();

    let mut context = DumpSymsContext::new(arch, &tpi, &ipi);

    let mut out = String::new();
    for sym in psi.names().iter(gss) {
        out.clear();
        // TODO: show the correct record offset, instead of 0
        dump_sym(&mut out, &mut context, 0, sym.kind, sym.data)?;
        println!("{out}");
    }

    println!();

    Ok(())
}
