use std::io::Write;
use std::path::{Path, PathBuf};

use super::ResultCache;

/// Saves a reduced file for a given `stage`
fn save_reduction(lines: &[String], path: &Path, stage: &str) {
    let mut path = path.to_path_buf();
    path.set_extension(format!("rs.{stage}"));
    let mut file = std::fs::File::create(&path).expect("Could not create the reduced example file");
    for line in lines {
        file.write_all(line.as_bytes()).expect("Could not save the reduced example");
    }
}

/// Checks if a given reduction is valid.
fn test_reduction(lines: &[String], path: &Path, cache: &mut ResultCache) -> bool {
    let mut path = path.to_path_buf();
    path.set_extension("rs_reduced");
    let mut file = std::fs::File::create(&path).expect("Could not create the reduced example file");
    for line in lines {
        file.write_all(line.as_bytes()).expect("Could not save the reduced example");
    }
    let res = super::test_cached(&path, false, cache);
    let Ok(Err(_)) = res else {
        return false;
    };
    true
}

/// Removes duplicate assignments in bulk.
/// If a line A = B is followed directly by A = C,
/// then removing the first line ought to be fully sound,
/// and not change the behaviour of the program at all. Detect & remove such lines.
fn remove_dup_assign(
    file: &mut Vec<String>,
    path: &PathBuf,
    starts: usize,
    ends: usize,
    cache: &mut ResultCache,
) {
    let mut file_copy = file.clone();
    let mut reduction_count = 0;
    // Not worth it.
    if ends - starts < 8 {
        return;
    }
    for index in starts..ends {
        let Some((prefix, _)) = file_copy[index].split_once('=') else {
            continue;
        };
        let Some((prefix2, postifx2)) = file_copy[index + 1].split_once('=') else {
            continue;
        };
        let prefix = prefix.trim();
        let prefix2 = prefix2.trim();
        // FIXME: Right now, remove_dup_assign cares about assignments to the exact same place.
        // However, given an assigemnt like this:
        // ```
        // A.0 = 1_u32;
        // A = (2_u32, 3.0);
        // ```
        // The first assignment could be safely omitted.
        // Additionally, we try to check if the second assignment could depend on the first one.
        // In such cases, the result is likely to change, so we bail.
        if prefix == prefix2 && !postifx2.contains(prefix) {
            file_copy[index] = "".into();
            reduction_count += 1;
        }
    }
    // We have removed no lines - no point in testing.
    if reduction_count == 0 {
        return;
    }
    // Check if the removed lines affected the execution result in any way, shape or form.
    if test_reduction(&file_copy, path, cache) {
        println!("Reduced {path:?} by {reduction_count} lines `remove_dup_assign`");
        *file = file_copy;
    } else {
        // The execution result changed.
        // This can occur if the second assignment depended on the first one.
        // Eg.
        // ```
        // a = b + c;
        // a = a + d;
        // ```
        remove_dup_assign(file, path, starts, (starts + ends) / 2, cache);
        remove_dup_assign(file, path, (starts + ends) / 2, ends, cache);
    }
    save_reduction(file, path, "remove_dup_assign");
}

/// Removes all the unneeded calls to `dump_var`. This is not something tools like `cvise` can do,
/// but it greately speeds up MIR interpretation + native execution.
fn remove_dump_var(file: &mut Vec<String>, path: &PathBuf) {
    let mut curr = 0;
    // ... try disabling `dump_vars` one by one, until only the necessary ones are left.
    while curr < file.len() {
        let Some(line) = file[curr..].iter().position(|line| line.contains("dump_var")) else {
            // No more `dump_var`s to remove - exit early.
            break;
        };
        // Make the line absolute again.
        let line = line + curr;
        let mut file_copy = file.clone();
        // Try removing 3 consecutive lines(the call, block end and block beginning). This effectively removes a `dump_var`.
        file_copy.remove(line);
        file_copy.remove(line);
        file_copy.remove(line);
        // Not cached - the execution result can change.
        let mut uncached = None;
        // Check if this reduction is valid.
        if test_reduction(&file_copy, path, &mut uncached) {
            println!("Reduced {path:?} by 3 lines `remove_dump_var`");
            *file = file_copy;
            curr = line;
        } else {
            curr = line + 1;
        }
    }
    save_reduction(file, path, "remove_dump_var");
}

/// Replaces matches with gotos where possible.
/// This exploits some properties of rustlantis(match arm order),
/// and is only soundly applicable to MIR generated by it.
/// Still, it is not something `cvise` can do, but it simplifies the code a ton.
fn match_to_goto(file: &mut Vec<String>, path: &PathBuf, cache: &mut ResultCache) {
    let mut curr = 0;

    while curr < file.len() {
        let Some(match_starts) = file[curr..].iter().position(|line| line.contains("match")) else {
            // No more `match`es to remove - exit early.
            break;
        };
        let match_starts = match_starts + curr;
        // Find the end of the match
        let Some(match_ends) = file[match_starts..].iter().position(|line| line.contains('}'))
        else {
            // Can't find match end - exit early.
            break;
        };
        let match_ends = match_ends + match_starts;
        let match_body = &file[match_starts..match_ends];

        // Find where this match should normally jump to.
        // This *should* be the second-last arm of the match, as per the paper(the remaining blocks are decoys).
        // If this ever changes, this reduction may not always be sound.
        // This is not a problem, however: we NEED to use MIRI for reduction anwyway,
        // and it will catch this issue.
        let jumps_to = &match_body[match_body.len() - 2].trim();
        let Some((_, bb_ident)) = jumps_to.split_once("bb") else {
            break;
        };
        // We now have the number of the block we jump to at runtime.
        let bb_ident = bb_ident.trim_matches(',');
        // Try replacing this match with an unconditional jump.
        let mut file_copy = file.clone();
        for _ in match_starts..(match_ends + 1) {
            file_copy.remove(match_starts);
        }
        file_copy.insert(match_starts, format!("Goto(bb{bb_ident})\n"));
        if test_reduction(&file_copy, path, cache) {
            println!("Reduced {path:?} by {} lines `match_to_goto`", match_ends - match_starts);
            *file = file_copy;
            curr = match_starts;
        } else {
            curr = match_ends;
        }
    }
    save_reduction(file, path, "match_to_goto");
}

/// At this point, we can try "killing" blocks, by replacing their bodies with calls to `abort`.
/// This is always sound(the program aborts, so no UB can occur after the block),
/// and allows us to safely remove *a lot* of unneeded blocks.
fn block_abort(file: &mut Vec<String>, path: &PathBuf, cache: &mut ResultCache) {
    let mut curr = 0;
    while curr < file.len() {
        let Some(block_starts) = file[curr..]
            .iter()
            .position(|line| line.starts_with("bb") && line.trim_end().ends_with(" = {"))
        else {
            // No more `block`s to kill - exit early.
            break;
        };
        let block_starts = block_starts + curr;
        // Find the beginning of the next block to find the end of this block.
        let Some(block_ends) = file[(block_starts + 1)..]
            .iter()
            .position(|line| line.starts_with("bb") && line.trim_end().ends_with(" = {"))
        else {
            // No more `block`s to kill - exit early.
            break;
        };
        let block_ends = block_starts + block_ends;
        let block_starts = block_starts + 1;
        let mut file_copy = file.clone();
        // Remove the block body...
        for _ in block_starts..(block_ends) {
            file_copy.remove(block_starts);
        }
        // ..and insert an unconditional call to abort.
        file_copy.insert(
            block_starts,
            "Call(tmp = core::intrinsics::abort(), ReturnTo(bb1), UnwindUnreachable())\n"
                .to_string(),
        );
        file_copy.insert(block_starts, "let tmp = ();\n".to_string());

        if test_reduction(&file_copy, path, cache) {
            println!("Reduced {path:?} by {} lines `block_abort`", block_ends - block_starts - 2);
            *file = file_copy;
            curr = block_starts;
        } else {
            curr = block_ends;
        }
    }
    save_reduction(file, path, "block_abort");
}

/// Removes unreachable basic blocks
fn remove_block(file: &mut Vec<String>, path: &PathBuf, cache: &mut ResultCache) {
    let mut curr = 0;

    // Next, we try to outright remove blocks.
    while curr < file.len() {
        let Some(block_starts) = file[curr..]
            .iter()
            .position(|line| line.starts_with("bb") && line.trim_end().ends_with(" = {"))
        else {
            // No more `block`s to remove - exit early.
            break;
        };
        let block_starts = block_starts + curr;
        // Find the beginning of the next block to find the end of this block.
        let Some(block_ends) = file[(block_starts + 1)..]
            .iter()
            .position(|line| line.starts_with("bb") && line.trim_end().ends_with(" = {"))
        else {
            // No more `block`s to remove - exit early.
            break;
        };
        let block_ends = block_starts + block_ends + 1;
        // Large blocks are likely to be necessary.
        if block_ends - block_starts > 6 {
            curr = block_starts + 1;
            continue;
        }
        let mut file_copy = file.clone();
        file_copy.drain(block_starts..block_ends);
        if test_reduction(&file_copy, path, cache) {
            println!("Reduced {path:?} by {} lines `remove_blocks`", block_ends - block_starts);
            *file = file_copy;
            curr = block_starts;
        } else {
            curr = block_starts + 1;
        }
    }
    save_reduction(file, path, "remove_block");
}

/// Merges blocks ending with unconditional jumps.
fn linearize_cf(file: &mut Vec<String>, path: &PathBuf, cache: &mut ResultCache) {
    let mut curr = 0;

    // Next, we try to linearize the control flow. What the does that mean?
    // Given a sequence like this:
    // Goto(bb22)
    // }
    // bb22 = {
    // We remove those 3 lines, merging the blocks together. This is not something `cvise` can do,
    // and it makes other transformations easier.
    while curr < file.len() {
        let Some(block_starts) = file[curr..]
            .iter()
            .position(|line| line.starts_with("bb") && line.trim_end().ends_with(" = {"))
        else {
            // No more `block`s to remove - exit early.
            break;
        };
        let block_starts = block_starts + curr;
        // Extract the block id.
        let Some((block, _)) = file[block_starts].split_once('=') else {
            curr = block_starts + 1;
            continue;
        };
        let block = block.trim();
        if file[block_starts - 2].trim() != format!("Goto({block})") {
            curr = block_starts + 1;
            continue;
        }
        let mut file_copy = file.clone();
        // Try removing 3 consecutive lines(the goto, block end and block beginning). This effectively removes a `Goto(next)`.
        file_copy.remove(block_starts - 2);
        file_copy.remove(block_starts - 2);
        file_copy.remove(block_starts - 2);
        // Check if this reduction is valid.
        if test_reduction(&file_copy, path, cache) {
            println!("Reduced {path:?} by 3 lines `linearize_cf`");
            *file = file_copy;
            curr = block_starts;
        } else {
            curr = block_starts + 1;
        }
    }
    save_reduction(file, path, "linearize_cf");
}

/// Replaces a call to a given function with a 0 assignment to the destination place, and a Goto.
/// This is always sound, because:
/// 1. All the functions arguments are always initialized
/// 2. and point to initialized  memory(the operand of &raw must be an initialized place in rustlantis).
fn remove_fn_calls(file: &mut Vec<String>, path: &PathBuf, cache: &mut ResultCache) {
    let mut curr = 0;

    while curr < file.len() {
        let Some(fn_call) =
            file[curr..].iter().position(|line| line.contains("Call(") && line.contains(" = fn"))
        else {
            // No more calls to remove - exit early.
            break;
        };
        let fn_call = fn_call + curr;
        let line = file[fn_call].trim();
        // Skip the Call(
        let line = &line["Call(".len()..];
        // Extract the destination place
        let Some((place, line)) = line.split_once('=') else {
            curr = fn_call + 1;
            continue;
        };
        // Skip till the return block id.
        let Some((_, line)) = line.split_once("ReturnTo(") else {
            curr = fn_call + 1;
            continue;
        };
        // Extract the full return block
        let Some((block, _)) = line.split_once(')') else {
            curr = fn_call + 1;
            continue;
        };
        let mut file_copy = file.clone();
        // Remove the call.
        file_copy.remove(fn_call);
        file_copy.insert(fn_call, format!("Goto({block})\n"));
        file_copy.insert(fn_call, format!("{place} = 0;\n"));
        // Check if this reduction is valid.
        if test_reduction(&file_copy, path, cache) {
            println!("Reduced {path:?} using `remove_fn_calls` {cache:?}");
            *file = file_copy;
            curr = fn_call;
        } else {
            curr = fn_call + 1;
        }
    }
    save_reduction(file, path, "remove_fn_calls");
}

/// Fully removes unreachable functions.
fn remove_fns(file: &mut Vec<String>, path: &PathBuf, cache: &mut ResultCache) {
    let mut curr = 0;

    while curr < file.len() {
        // Find a function start
        let Some(fn_start) = file[curr..].iter().position(|line| {
            line.contains("#[custom_mir(dialect = \"runtime\", phase = \"initial\")]")
        }) else {
            // No more functions to remove - exit early.
            break;
        };
        // Find the next function(and use that to find the end of this one).
        // FIXME: this check is flawed: it will never remove the very last function(the one before main).
        // The other checks will turn that function into a single call to abort, but it is still annoying that it is kept.
        let fn_start = fn_start + curr;
        let Some(fn_end) = file[(fn_start + 3)..].iter().position(|line| line.contains("fn fn"))
        else {
            // No more functions to remove - exit early.
            break;
        };
        let fn_end = fn_start + 2 + fn_end;
        let mut file_copy = file.clone();
        // Remove the function.\\
        file_copy.drain(fn_start..fn_end);
        // Check if this reduction is valid.
        if test_reduction(&file_copy, path, cache) {
            println!("Reduced {path:?} by {} lines `remove_fns`", fn_end - fn_start);
            *file = file_copy;
        } else {
            curr = fn_start + 1;
        }
    }
    save_reduction(file, path, "remove_fns");
}

pub(super) fn reduce(path: impl AsRef<Path>) {
    let path = path.as_ref().to_owned();
    // ... read the file to a buffer ..
    let file = std::fs::read_to_string(&path).expect("Could not open the file to reduce");
    let mut file: Vec<_> = file.split_inclusive('\n').map(|s| s.to_string()).collect();

    // ... and run reduction passes.
    println!("running `remove_dump_var` on {path:?}.");
    remove_dump_var(&mut file, &path);
    // After `dump_var`, the execution results ought not to change. Cache them.
    let mut cache = None;
    // Fill the cache
    assert!(
        test_reduction(&file, &path, &mut cache),
        "Reduction error: check that the input file is a valid reproducer."
    );
    println!("cache:{cache:?}");
    println!("running `remove_fn_calls` on {path:?}.");
    remove_fn_calls(&mut file, &path, &mut cache);
    println!("running `remove_fns` on {path:?}.");
    remove_fns(&mut file, &path, &mut cache);
    let len = file.len();
    println!("running `remove_dup_assign` on {path:?}.");
    remove_dup_assign(&mut file, &path, 0, len, &mut cache);
    file.retain(|line| !line.is_empty());
    println!("running `match_to_goto` on {path:?}.");
    match_to_goto(&mut file, &path, &mut cache);
    println!("running `block_abort` on {path:?}.");
    block_abort(&mut file, &path, &mut cache);
    println!("running `remove_block` on {path:?}.");
    remove_block(&mut file, &path, &mut cache);
    println!("running `linearize_cf` on {path:?}.");
    linearize_cf(&mut file, &path, &mut cache);
    let mut out = std::fs::File::create(&path).expect("Could not save the reduction result.");
    let file = file.into_iter().collect::<String>();
    out.write_all(file.as_bytes()).expect("failed to write into file");
}
