use std::collections::HashMap;

use camino::{Utf8Path, Utf8PathBuf};
use libpijul::ArcTxn;
use libpijul::ChannelRef;
use libpijul::changestore::ChangeStore;
use libpijul::pristine::sanakirja::MutTxn;
use libpijul::working_copy::WorkingCopy;
use libpijul::working_copy::WorkingCopyRead;

use crate::file_system::changes::unrecorded::UnrecordedChangesError;
use crate::file_system::changes::unrecorded::UnrecordedState;
use crate::file_system::open_file::OpenFile;

#[derive(Debug, thiserror::Error)]
pub enum UpdateOpenFileContentsError {
    #[error("No file open at {0}")]
    NoMatchingFile(Utf8PathBuf),
}

#[derive(Debug, thiserror::Error)]
pub enum UpdateOpenFileCreditsError<C: std::error::Error + 'static, W: std::error::Error + 'static>
{
    #[error("No file open at {0}")]
    NoMatchingFile(Utf8PathBuf),
    #[error(transparent)]
    UnrecordedChanges(#[from] UnrecordedChangesError<C, W>),
}

#[derive(Clone)]
pub struct EditorWorkingCopy<W: WorkingCopy + Clone + Send + Sync + 'static> {
    pub working_copy: W,
    pub open_files: HashMap<Utf8PathBuf, OpenFile>,
}

impl<W: WorkingCopy + Clone + Send + Sync + 'static> EditorWorkingCopy<W> {
    pub fn new(working_copy: W) -> Self {
        Self {
            working_copy,
            open_files: HashMap::new(),
        }
    }

    pub fn update_open_file_contents(
        &mut self,
        path: &Utf8Path,
        character_offset: usize,
        characters_replaced: usize,
        replacement_text: &str,
    ) -> Result<(), UpdateOpenFileContentsError> {
        let open_file = self
            .open_files
            .get_mut(path)
            .ok_or_else(|| UpdateOpenFileContentsError::NoMatchingFile(path.to_path_buf()))?;

        if characters_replaced > 0 {
            open_file
                .contents
                .text
                .remove(character_offset..(character_offset + characters_replaced));
        }

        if !replacement_text.is_empty() {
            open_file
                .contents
                .text
                .insert(character_offset, replacement_text);
        }

        Ok(())
    }

    pub fn update_open_file_credits<C>(
        &mut self,
        path: &Utf8Path,
        transaction: &ArcTxn<MutTxn<()>>,
        channel: &ChannelRef<MutTxn<()>>,
        change_store: &C,
    ) -> Result<(), UpdateOpenFileCreditsError<C::Error, W::Error>>
    where
        C: ChangeStore + Clone + Send + 'static,
    {
        if !self.open_files.contains_key(path) {
            return Err(UpdateOpenFileCreditsError::NoMatchingFile(
                path.to_path_buf(),
            ));
        }

        let unrecorded_state = match self.open_files.get(path).unwrap().credits.is_some() {
            true => Some(UnrecordedState::new(
                path,
                transaction,
                channel,
                change_store,
                self,
            )?),
            false => None,
        };

        if let Some(file_credits) = &mut self.open_files.get_mut(path).unwrap().credits {
            file_credits.unrecorded_state = unrecorded_state.unwrap();
        }

        let open_file = self.open_files.get_mut(path).unwrap();
        if let Some(file_credits) = &mut open_file.credits {
            file_credits.spans = file_credits.recompute_spans(&open_file.contents);
        }

        Ok(())
    }
}

impl<W: WorkingCopy + Clone + Send + Sync + 'static> WorkingCopyRead for EditorWorkingCopy<W> {
    type Error = W::Error;

    fn file_metadata(&self, file: &str) -> Result<libpijul::pristine::InodeMetadata, Self::Error> {
        self.working_copy.file_metadata(file)
    }

    fn read_file(&self, file: &str, buffer: &mut Vec<u8>) -> Result<(), Self::Error> {
        match self.open_files.get(Utf8Path::new(file)) {
            Some(open_file) => buffer.extend(open_file.contents.text.bytes()),
            None => self.working_copy.read_file(file, buffer)?,
        }

        Ok(())
    }

    fn modified_time(&self, file: &str) -> Result<std::time::SystemTime, Self::Error> {
        match self.open_files.get(Utf8Path::new(file)) {
            Some(open_file) => Ok(open_file.contents.modified_time.into()),
            None => self.working_copy.modified_time(file),
        }
    }
}