use std::{ffi::OsString, future::Future};

/// Describes how a list (of commits or of files) changed.
#[derive(Clone, Debug)]
pub struct ListChange {
    /// Index of the start of the changed data.
    pub position: usize,
    /// Number of removed lines at the `position`.
    pub removed: usize,
    /// Number of added lines at the `position`.
    pub added: usize,
}

/// The trait the UI should implement to bind to the workspace.
pub trait Frontend: Clone {
    /// Spawn (run) the given future to run asynchronously in background. Note the future
    /// does not necessarily implement Send; meaning it has to be run on the current thread.
    fn spawn_future(&self, future: impl Future<Output = ()> + 'static);

    /// Called to terminate the application after everything completed with the given exit
    /// code.
    fn terminate(&self, exit_code: u8);

    /// Called when the workspace is in the process of changing the current commit. Until
    /// this finishes (which is indicated by a call to `commit_applied` or `commit_failed`),
    /// no calls to `Workspace` methods are allowed. It is best to just show a busy indication
    /// to the user and don't allow any interaction.
    fn processing_commit(&self);

    /// Called when application of the current commit finished. Note this doesn't mean
    /// all blocks are resolved; AI may still be running. But the current commit is
    /// presentable at this point and the frontend should allow user interaction. Note that as
    /// a special case, if workspace.is_clean_cherry_pick() returns true, the commit is also
    /// fully resolved. `commit_index` contains the current commit index; the commit list
    /// selected item should be changed to this.
    fn commit_applied(&self, commit_index: usize);

    /// Called when application of the current commit failed. The current commit was rewinded
    /// to the previous one (i.e. the current commit in the moment of the call is not the one
    /// that failed). It's acceptable for the frontend to give up when this happens. If
    /// `commit_index` is `Some`, its meaning is the same as in `commit_applied`. If it is
    /// `None`, there are no commits applied. That case is especially hard to recover from,
    /// since many methods of `Workspace` will panic when in this state.
    fn commit_failed(&self, err_msg: String, commit_index: Option<usize>);

    /// Called when the commit list changed and needs to be redrawn. `selected` indicates
    /// the index of the commit that should be selected after modifying the list or None if
    /// no commit should be selected. This is merely a visual aid and is not necessarily the
    /// current commit (e.g. it may already be the next commit during commit application).
    /// The frontend may assume the commit list is never empty.
    fn commit_list_changed(&self, change: ListChange, selected: Option<usize>);

    /// Called when a file changed. This does not include purely visual changes (folding
    /// a block, updating suggestions). This does include approving a block and of course
    /// changing the content of the file.
    fn file_changed(&self, file_index: usize);

    /// Called when some lines changed and need to be redrawn. `file_index` is the index to
    /// `workspace.commit.files`.
    fn view_changed(&self, file_index: usize, change: ListChange);

    /// Call the editor. `path` is the path of the actual file; however, this file should not
    /// be edited. It's included for reference to infer the syntax highlighting etc. for the
    /// edited snippet. The actual data to edit are in `data`. When finished, the UI should
    /// call `done` and pass to it either `None` (if the edit was unsuccessful or aborted) or
    /// `Some(resulting_data)`.
    fn run_editor(
        &self,
        path: OsString,
        data: Vec<u8>,
        done: Box<dyn FnOnce(Option<Vec<u8>>)>,
    );

    /// Get the telemetry-id if the user previously agreed to telemetry collection.
    /// Ask them if we haven't asked before. Return a stable uuid or None if we shouldn't
    /// submit the data at this point (or ever).
    #[allow(async_fn_in_trait)]
    async fn telemetry_agreement(&self) -> Option<uuid::Uuid>;
}

impl ListChange {
    pub(crate) fn changed(position: usize, removed: usize, added: usize) -> Self {
        Self {
            position,
            removed,
            added,
        }
    }

    pub(crate) fn added(position: usize, added: usize) -> Self {
        Self {
            position,
            removed: 0,
            added,
        }
    }

    pub(crate) fn removed(position: usize, removed: usize) -> Self {
        Self {
            position,
            removed,
            added: 0,
        }
    }

    pub(crate) fn updated_item(position: usize) -> Self {
        Self {
            position,
            removed: 1,
            added: 1,
        }
    }
}
