use std::io;

use crate::address::{AddressSpace, AddressValue};
use crate::db::{Equivalent, Error, Function};
use serde::{Deserialize, Serialize};

pub trait Environment {
    fn load_file_bytes(
        &self,
        file: &str,
        offset: usize,
        size: AddressValue,
    ) -> Result<Vec<u8>, io::Error>;
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Command {
    SetLabel {
        space: AddressSpace,
        offset: AddressValue,
        label: Option<String>,
    },
    SetEquivalent {
        space: AddressSpace,
        offset: AddressValue,
        equivalent: Equivalent,
    },
    AutoDisassemble {
        space: AddressSpace,
        start: AddressValue,
    },
    ClearEquivalents {
        space: AddressSpace,
        offset: AddressValue,
        size: AddressValue,
    },
    SetComment {
        space: AddressSpace,
        offset: AddressValue,
        comment: Option<String>,
    },
    MapBytes {
        space: AddressSpace,
        offset: AddressValue,
        file: String,
        file_offset: usize,
        size: AddressValue,
    },
    ClearBytes {
        space: AddressSpace,
        offset: AddressValue,
        size: AddressValue,
    },
    SetConstantBytes {
        space: AddressSpace,
        offset: AddressValue,
        size: AddressValue,
        value: u8,
    },
    SetFunction {
        space: AddressSpace,
        offset: AddressValue,
        function: Option<Function>,
    },
}

impl Command {
    pub fn set_label(space: AddressSpace, offset: AddressValue, label: impl Into<String>) -> Self {
        Self::SetLabel {
            space,
            offset,
            label: Some(label.into()),
        }
    }

    pub fn clear_label(space: AddressSpace, offset: AddressValue) -> Self {
        Self::SetLabel {
            space,
            offset,
            label: None,
        }
    }

    pub fn auto_disassemble(space: AddressSpace, start: AddressValue) -> Self {
        Self::AutoDisassemble { space, start }
    }

    pub fn set_equivalent(
        space: AddressSpace,
        offset: AddressValue,
        equivalent: Equivalent,
    ) -> Self {
        Self::SetEquivalent {
            space,
            offset,
            equivalent,
        }
    }

    pub fn clear_equivalents(
        space: AddressSpace,
        offset: AddressValue,
        size: AddressValue,
    ) -> Self {
        Self::ClearEquivalents {
            space,
            offset,
            size,
        }
    }

    pub fn set_comment(
        space: AddressSpace,
        offset: AddressValue,
        comment: impl Into<String>,
    ) -> Self {
        Self::SetComment {
            space,
            offset,
            comment: Some(comment.into()),
        }
    }

    pub fn clear_comment(space: AddressSpace, offset: AddressValue) -> Self {
        Self::SetComment {
            space,
            offset,
            comment: None,
        }
    }

    pub fn map_bytes(
        space: AddressSpace,
        offset: AddressValue,
        file: impl Into<String>,
        file_offset: usize,
        size: AddressValue,
    ) -> Self {
        Self::MapBytes {
            space,
            offset,
            file: file.into(),
            file_offset,
            size,
        }
    }

    pub fn clear_bytes(space: AddressSpace, offset: AddressValue, size: AddressValue) -> Self {
        Self::ClearBytes {
            space,
            offset,
            size,
        }
    }

    pub fn set_constant_bytes(
        space: AddressSpace,
        offset: AddressValue,
        size: AddressValue,
        value: u8,
    ) -> Self {
        Self::SetConstantBytes {
            space,
            offset,
            size,
            value,
        }
    }

    pub fn set_function(space: AddressSpace, offset: AddressValue, function: Function) -> Self {
        Self::SetFunction {
            space,
            offset,
            function: Some(function),
        }
    }

    pub fn clear_function(space: AddressSpace, offset: AddressValue) -> Self {
        Self::SetFunction {
            space,
            offset,
            function: None,
        }
    }

    pub fn apply(
        self,
        db: &mut crate::db::Db,
        env: Option<&dyn Environment>,
    ) -> Result<Vec<Command>, crate::db::Error> {
        match self {
            Command::SetLabel {
                space,
                offset,
                label,
            } => {
                let region = db.region_mut(space);
                let before = region.get_label(offset).map(str::to_owned);
                match label.as_deref() {
                    Some(label) => region.set_label(offset, label),
                    None => region.clear_label(offset),
                }
                Ok(vec![Command::SetLabel {
                    space,
                    offset,
                    label: before,
                }])
            }
            Command::SetEquivalent {
                space,
                offset,
                equivalent,
            } => {
                let region = db.region_mut(space);
                let span = region.equivalent_span(offset, &equivalent)?;
                let before = region.snapshot_equivalents(offset, span);
                region.set_equivalent(offset, equivalent)?;
                let mut undo = vec![Command::ClearEquivalents {
                    space,
                    offset,
                    size: span,
                }];
                for (start, range) in before {
                    undo.push(Command::SetEquivalent {
                        space,
                        offset: start,
                        equivalent: range.equivalent,
                    });
                }
                Ok(undo)
            }
            Command::AutoDisassemble { space, start } => {
                let region = db.region_mut(space);
                let commands = region.auto_disassemble(start);
                Ok(commands
                    .into_iter()
                    .map(|addr| Command::ClearEquivalents {
                        space,
                        offset: addr,
                        size: 1,
                    })
                    .collect())
            }
            Command::ClearEquivalents {
                space,
                offset,
                size,
            } => {
                let region = db.region_mut(space);
                let before = region.snapshot_equivalents(offset, size);
                region.clear_equivalents(offset, size);
                let mut undo = Vec::new();
                for (start, range) in before {
                    undo.push(Command::SetEquivalent {
                        space,
                        offset: start,
                        equivalent: range.equivalent,
                    });
                }
                Ok(undo)
            }
            Command::SetComment {
                space,
                offset,
                comment,
            } => {
                let region = db.region_mut(space);
                let before = region.get_comment(offset).map(str::to_owned);
                match comment.as_deref() {
                    Some(comment) => region.set_comment(offset, comment),
                    None => region.clear_comment(offset),
                }
                Ok(vec![Command::SetComment {
                    space,
                    offset,
                    comment: before,
                }])
            }
            Command::MapBytes {
                space,
                offset,
                file,
                file_offset,
                size,
            } => {
                let region = db.region_mut(space);
                let Some(env) = env else {
                    return Err(Error::NoEnvironment);
                };
                let bytes = env
                    .load_file_bytes(&file, file_offset, size)
                    .map_err(Error::Io)?;
                let size = bytes.len() as AddressValue;
                let before = region.snapshot_byte_ranges(offset, size);
                region.map_bytes(&file, file_offset, offset, &bytes);
                Ok(undo_byte_ranges(space, offset, size, before))
            }
            Command::ClearBytes {
                space,
                offset,
                size,
            } => {
                let region = db.region_mut(space);
                let before = region.snapshot_byte_ranges(offset, size);
                region.clear_bytes(offset, size);
                Ok(undo_byte_ranges(space, offset, size, before))
            }
            Command::SetConstantBytes {
                space,
                offset,
                size,
                value,
            } => {
                let region = db.region_mut(space);
                let before = region.snapshot_byte_ranges(offset, size);
                region.set_constant(offset, size, value);
                Ok(undo_byte_ranges(space, offset, size, before))
            }
            Command::SetFunction {
                space,
                offset,
                function,
            } => {
                let region = db.region_mut(space);
                let before = region.get_function(offset).cloned();
                match function {
                    Some(function) => region.set_function(function),
                    None => region.clear_function(offset),
                }
                Ok(vec![Command::SetFunction {
                    space,
                    offset,
                    function: before,
                }])
            }
        }
    }
}

fn undo_byte_ranges(
    space: AddressSpace,
    offset: AddressValue,
    size: AddressValue,
    ranges: Vec<(AddressValue, crate::region::ByteRange)>,
) -> Vec<Command> {
    use crate::region::ByteRange;

    let mut undo = vec![Command::ClearBytes {
        space,
        offset,
        size,
    }];
    for (start, range) in ranges {
        match range {
            ByteRange::Mapped(file, file_offset, data) => {
                undo.push(Command::MapBytes {
                    space,
                    offset: start,
                    file,
                    file_offset,
                    size: data.len() as AddressValue,
                });
            }
            ByteRange::Constant(count, value) => {
                undo.push(Command::SetConstantBytes {
                    space,
                    offset: start,
                    size: count as AddressValue,
                    value,
                });
            }
        }
    }
    undo
}
