use bstr::ByteSlice;
use crate::common::{apply, diff_meta, git, git::Git, history, history_git };
use crate::common::lines::Lines;
use futures_concurrency::prelude::*;
use std::{collections::HashSet, ffi::OsStr, ffi::OsString, future::Future, rc::Rc};
use std::cell::{Cell, Ref, RefCell, RefMut};
use std::ops::{Deref, Range};
use std::os::unix::ffi::OsStrExt;
use super::{rpc::Rpc, tasks::Tasks, telemetry::Telemetry};
use super::blocks::{Block, Blocks, Commands, SuggestionSearchState};
use super::commit::{
    Commit, CommitFile, CommitMetadata, FileFromConflict, FileFromMergetool, FileFromUpstream,
};
use super::commits::{CommitIndex, Commits, NoCommit};
use super::filesystem::{
    FilesystemWrite, RealFilesystem, SharedVirtualFilesystem, VirtualFilesystem,
};
use super::format::{Formatted, Style};
use super::frontend::{Frontend, ListChange};
use super::git_utils::ToDescription;
use super::unique_id::{UniqueId, UniqueIdGenerator};

#[derive(Clone, Copy, PartialEq)]
pub enum StoreMode {
    /// Backporting a commit. The result should be committed.
    Commit,
    /// Patchpal custom user friendly merge resolution. The result should be staged.
    Stage(MergeOp),
    /// Invoked as git mergetool. The result should be just saved.
    Save,
}

/// The git operation that caused the merge conflict.
#[derive(Clone, Copy, PartialEq)]
pub enum MergeOp {
    None,
    CherryPick,
    Rebase,
}

/// Override of normal operation. This is requested by user for debugging and test cases.
/// A value always includes all former values; in other words, `AllClean` implies
/// `NoTelemetry` and `NoAI`.
#[derive(Clone, Copy, PartialEq, PartialOrd)]
pub enum Overrides {
    /// No override.
    None,
    /// Do not submit telemetry and do not ask for opting in.
    NoTelemetry,
    /// Do not call AI inference. Resolve content fuzz by simply replacing the content by
    /// the original added lines.
    NoAI,
    /// Pretend everything is clean, even when it is not.
    AllClean,
}

/// The main struct holding the code and modifications. All the work being done is stored
/// here. `Workspace` is reference counted and is expected to be cloned heavily. The methods
/// take immutable references, even for mutating operations. Note that `Workspace` derefs to
/// `WorkspaceInner`.
pub struct Workspace<FE: Frontend>(Rc<WorkspaceInner<FE>>);

pub struct WorkspaceInner<FE: Frontend> {
    frontend: FE,
    pub upstream_repo: Git,
    downstream_repo: Git,
    overrides: Overrides,
    unique_id_generator: UniqueIdGenerator,
    rpc: RefCell<Rpc>,
    commit_headers: Vec<String>,
    pub store_mode: Cell<StoreMode>,
    commits: Commits,
    current_commit_index: Cell<CommitIndex>,
    modified: Cell<bool>,
    background_tasks: Tasks,
    terminating: Cell<bool>,
    telemetry: Telemetry,
}

impl MergeOp {
    fn detect(repo: &Git) -> (Self, Option<Rc<git::Description>>) {
        if let Ok(desc) = b"CHERRY_PICK_HEAD".to_description(repo) {
            (Self::CherryPick, Some(desc))
        } else if let Ok(desc) = b"REBASE_HEAD".to_description(repo) {
            (Self::Rebase, Some(desc))
        } else {
            (Self::None, None)
        }
    }
}

impl<FE: Frontend + 'static> Workspace<FE> {
    pub fn new(
        frontend: FE,
        upstream_repo: Git,
        downstream_repo: Git,
        overrides: Overrides,
        rpc: Rpc,
        commit_headers: Vec<String>,
    ) -> Self {
        let background_tasks = Tasks::new();
        frontend.spawn_future(background_tasks.clone());
        let telemetry = Telemetry::new();
        telemetry.set_repo_type(&downstream_repo);
        telemetry.tele_set("mode", "backport");
        Self(Rc::new(WorkspaceInner {
            frontend,
            upstream_repo,
            downstream_repo,
            overrides,
            unique_id_generator: UniqueIdGenerator::new(),
            rpc: RefCell::new(rpc),
            commit_headers,
            store_mode: Cell::new(StoreMode::Commit),
            commits: Commits::default(),
            current_commit_index: Cell::new(CommitIndex::default()),
            modified: Cell::new(false),
            background_tasks,
            terminating: Cell::new(false),
            telemetry,
        }))
    }

    /// Add the commit with the given description. Panics if there has been a merge conflict
    /// or mergetool already added.
    pub fn push_commit(&self, description: impl ToDescription) -> Result<(), String> {
        self.insert_commit(self.commits.len(), description)
    }

    /// Inserts a commit to the given `index`. Panics if the current commit is at or past the
    /// index. Panics if the index is too large. Panics if there has been a merge conflict
    /// or mergetool already added.
    pub fn insert_commit(&self, index: usize, description: impl ToDescription)
        -> Result<(), String>
    {
        if self.commits.len() == 0 {
            self.ensure_downstream_clean()?;
        }
        let description = description.to_description(&self.upstream_repo)?;
        let file_changes = self.upstream_repo.raw_diff(&description.id)
                .map_err(|err| format!(
                    "Cannot find commit {}: {}",
                    &description.id.as_bstr(),
                    err.to_string()
                ))?;
        if file_changes.len() == 0 {
            return Err(format!("Merge commits are not supported."));
        }
        let mut commit = Commit::with_capacity(
            file_changes.len(),
            Some(CommitMetadata::new(
                description,
                self.upstream_repo.clone(),
                self.downstream_repo.clone(),
            )),
        );
        for change in file_changes {
            commit.push_file(change, FileFromUpstream::new());
        }
        commit.sort_files();
        self.reinsert_commit(index, commit);
        Ok(())
    }

    /// Reinsert a commit obtained by `remove_commit` to `index`. See `insert_commit` for
    /// limitations.
    pub fn reinsert_commit(&self, index: usize, commit: Commit) {
        assert!(self.store_mode.get() == StoreMode::Commit);
        assert!(self.current_commit_index.get() < index.into());
        self.commits.insert(index.into(), commit);
        self.frontend.commit_list_changed(
            ListChange::added(index, 1),
            self.current_commit_index.get().try_index(),
        );
    }

    /// Remove the commit at the given `index` and returns it. Panics if the current commit
    /// is at or past the index. Panics if the index is too large. Panics if there has been a
    /// merge conflict or mergetool already added.
    pub fn remove_commit(&self, index: usize) -> Commit {
        assert!(self.store_mode.get() == StoreMode::Commit);
        assert!(self.current_commit_index.get() < index.into());
        let commit = self.commits.remove(index.into()).unwrap();
        self.frontend.commit_list_changed(
            ListChange::removed(index, 1),
            self.current_commit_index.get().try_index(),
        );
        commit
    }

    /// Attempt to add the merge conflicts from downstrea_repo to the commits. Returns
    /// `Ok(false)` if there are no merge conflicts. Panics if there has been already a
    /// commit added before.
    pub fn push_merge_conflicts(&self) -> Result<bool, String> {
        let conflicts = self.downstream_repo.conflicts()
            .map_err(|e| format!("Cannot get git repository status: {e}"))?;
        if conflicts.is_empty() {
            return Ok(false);
        }
        let (merge_op, description) = MergeOp::detect(&self.downstream_repo);
        self.store_mode.set(StoreMode::Stage(merge_op));
        self.telemetry.tele_set("mode", "mergeport");
        let mut commit = Commit::with_capacity(
            conflicts.len(),
            description.map(|description|
                CommitMetadata::new(
                    description,
                    self.upstream_repo.clone(),
                    self.downstream_repo.clone(),
                )
            ),
        );
        for (dst, src) in conflicts {
            commit.push_file(src, FileFromConflict::new(dst));
        }
        commit.sort_files();
        let new_len = self.commits.push(commit);
        assert!(new_len == 1);
        self.frontend.commit_list_changed(ListChange::added(0, 1), Some(0));
        Ok(true)
    }

    /// Add the mergetool provided conflict. Panics if there has been already a commit added
    /// before.
    pub fn push_mergetool(
        &self,
        base: &OsStr,
        local: &OsStr,
        remote: &OsStr,
        merged: &OsStr,
    ) -> Result<(), String> {
        self.store_mode.set(StoreMode::Save);
        self.telemetry.tele_set("mode", "mergetool");
        let file_change = diff_meta::FileChange::from_files(base, remote, merged)
            .map_err(|err| err.to_string())?;
        let mut commit = Commit::with_capacity(1, None);
        commit.push_file(
            file_change,
            FileFromMergetool::new(local.as_bytes(), merged.as_bytes()),
        );
        let new_len = self.commits.push(commit);
        assert!(new_len == 1);
        self.frontend.commit_list_changed(ListChange::added(0, 1), Some(0));
        Ok(())
    }

    /// Signal that the initial addition of commits completed and the first commit should
    /// get applied. Panics if there are no commits.
    pub fn start(&self) -> Result<(), String> {
        assert!(self.current_commit_index.get().no_index());
        self.set_basic_telemetry_stats();
        self.submit_telemetry();
        assert!(self.apply_next()?);
        Ok(())
    }

    /// Set the current commit index to `new_index` and start applying that commit. On
    /// completion, `Frontend::commit_applied` or `Frontend::commit_failed` will be called.
    /// Panics if any of the commits preceding `new_index` are not applied or are not resolved
    /// (AI or edit is in progress or failed). Panics if `new_index` does not have an index
    /// set.
    fn set_current_commit(&self, new_index: CommitIndex) -> Result<(), String> {
        self.frontend.processing_commit();
        let prev_index = self.current_commit_index.get();
        // Cache the resolution of all applied commits that we're leaving behind (if there are
        // such).
        for i in self.current_commit_index.get().iter_after(new_index) {
            self.commits.get_mut(i).unwrap().cache_blocks();
        }
        self.current_commit_index.set(new_index);
        // Build the virtual subsystem with all commits except for the current one written
        // into it.
        let mut filesystem = VirtualFilesystem::new(self.downstream_repo.clone());
        for i in new_index.iter_until() {
            self.write_files(&mut filesystem, &self.commits.get(i).unwrap())?;
        }
        // If we're unapplying everything, we're done. Note we can't call commit_applied in
        // this case.
        if new_index.no_index() {
            return Ok(());
        }
        // If we're going backwards, write also the current commit and be done.
        if prev_index > new_index {
            self.write_files(&mut filesystem, &self.commits.get(new_index).unwrap())?;
            self.frontend.commit_applied(new_index.index());
            return Ok(());
        }
        // Apply the files in threads.
        let running = self.commits.get(new_index).unwrap().apply_files(
            &SharedVirtualFilesystem::new(filesystem),
            &self.upstream_repo,
        );
        let workspace = self.clone();
        self.frontend.spawn_future(async move {
            match running.try_join().await {
                Ok(all_applied) => {
                    for (file_index, applied) in all_applied.into_iter().enumerate() {
                        workspace.create_blocks(file_index, applied);
                    }
                    workspace.rpc_mut().execute(workspace.frontend.clone());
                    workspace.frontend.commit_applied(new_index.index());
                }
                Err(msg) => {
                    let prev_index = new_index.prev();
                    workspace.current_commit_index.set(prev_index);
                    workspace.frontend.commit_failed(msg, prev_index.try_index());
                }
            }
        });
        Ok(())
    }

    /// Switch to the commit at `index` and start applying it. See `set_current_commit` for
    /// details on the asynchronous completion and preconditions.
    pub fn jump_to_commit(&self, index: usize) -> Result<(), String> {
        let new_index = CommitIndex::from(index);
        if self.current_commit_index.get() == new_index {
            return Ok(());
        }
        self.set_current_commit(new_index)
    }

    /// Rewind before the given commit index, ensuring the current commit is before the
    /// `index`. If that's already the case, return `Ok(false)` and do nothing. Otherwise,
    /// set the current commit, start applying it and return `Ok(true)`.
    pub fn jump_before_commit(&self, index: usize) -> Result<bool, String> {
        let new_index = CommitIndex::from(index);
        if self.current_commit_index.get() < new_index {
            return Ok(false);
        }
        self.set_current_commit(new_index.prev()).map(|_| true)
    }

    /// Advance the current commit index and start applying the next commit. On completion,
    /// `Frontend::commit_applied` or `Frontend::commit_failed` will be called. Return
    /// `Ok(false)` if there are no more commits left. Return `Err` in case of error.
    pub fn apply_next(&self) -> Result<bool, String> {
        let new_index = self.current_commit_index.get().next();
        if !new_index.within(self.commits.len()) {
            return Ok(false);
        }
        self.set_current_commit(new_index)?;
        Ok(true)
    }

    /// Go back to the previous commit index and start applying it. On completion,
    /// `Frontend::commit_applied` or `Frontend::commit_failed` will be called. Return
    /// `Ok(false)` if already at the first commit. Return `Err` in case of error.
    pub fn apply_prev(&self) -> Result<bool, String> {
        let new_index = self.current_commit_index.get().prev();
        if new_index.no_index() {
            return Ok(false);
        }
        self.set_current_commit(new_index)?;
        Ok(true)
    }

    fn create_blocks(
        &self,
        file_index: usize,
        downstream_data: apply::Applied,
    ) {
        let mut commit = self.commit_mut().unwrap();
        commit.files[file_index].blocks = Some(Blocks::new(
            downstream_data.old_lines,
            self.unique_id_generator.clone(),
        ));
        let acked_clean = match self.store_mode.get() {
            StoreMode::Commit => false,
            StoreMode::Stage(_) => true,
            StoreMode::Save => true,
        };
        let mut last_line = 0.into();
        for change in downstream_data.changes {
            let range = change.range.clone();
            let suggestions =
                commit.has_metadata()
                && change.fuzz != apply::AppliedFuzz::None;
            // Always separate change blocks by a ground block, even if such ground block
            // is empty. There's also always a ground block before the first change block
            // and after the last change block. This makes it much easier to unapply
            // change blocks.
            let blocks = commit.files[file_index].blocks.as_mut().unwrap();
            let block = blocks.create_ground(last_line .. range.start, true);
            blocks.push(block);
            let fuzz_content =
                change.fuzz == apply::AppliedFuzz::Content &&
                self.overrides < Overrides::NoAI;
            let cached = commit.files[file_index].pop_cache(&change);
            let blocks = commit.files[file_index].blocks.as_mut().unwrap();
            let block_index = if let Some(entry) = cached {
                let block = blocks.create_from_cache(entry, change);
                blocks.push(block)
            } else if fuzz_content {
                let block = blocks.create_interim(change);
                let block_index = blocks.push(block);
                let block = blocks.get(block_index);
                let target = self.clone();
                let unique_id = block.unique_id;
                self.rpc_mut().queue_ai(
                    blocks.old_lines_slice(block_index),
                    &block.as_change().hunk,
                    move |r| target.populate_ai_result(unique_id, true, r),
                    self.frontend.clone(),
                );
                block_index
            } else {
                let block = blocks.create_change(change, acked_clean);
                blocks.push(block)
            };
            if suggestions {
                let block = blocks.get_mut(block_index);
                block.set_suggestion_search_state(SuggestionSearchState::InProgress);
                let unique_id = block.unique_id;
                let change = block.as_change();
                let upstream_range =
                    change.hunk.new.start.export() .. change.hunk.new.end.export();
                let downstream_range =
                    change.range.start.export() .. change.range.end.export();
                self.queue_suggestions_search(
                    &commit,
                    file_index,
                    unique_id,
                    upstream_range,
                    downstream_range,
                );
            }
            last_line = range.end;
        }
        let blocks = commit.files[file_index].blocks.as_mut().unwrap();
        let block = blocks.create_ground(
            last_line .. blocks.old_lines.line_end(),
            true,
        );
        blocks.push(block);
        for hunk in downstream_data.unapplied {
            let block = blocks.create_unapplied(hunk, false);
            blocks.push(block);
        }
    }

    fn queue_suggestions_search(
        &self,
        commit: &Commit,
        file_index: usize,
        unique_id: UniqueId,
        upstream_range: Range<usize>,
        downstream_range: Range<usize>,
    ) {
        let file = &commit.files[file_index];
        let file_history = commit.file_history(file, &self.upstream_repo);
        self.frontend.spawn_future(history::spawn_missing_searcher(
            self.clone(),
            unique_id,
            history_git::missing_searcher,
            commit.description().id.clone(),
            self.upstream_repo.clone(),
            self.downstream_repo.clone(),
            file.upstream_metadata.name.clone(),
            file.blocks.as_ref().unwrap().old_lines.path.clone(),
            upstream_range,
            downstream_range,
            commit.fork_point().clone(),
            file_history.clone(),
        ));
    }

    pub fn handle_command(
        &self,
        file_index: usize,
        block_index: usize,
        command: Commands,
    ) -> Result<(), String> {
        match command {
            Commands::ACK => self.command_ack(file_index, block_index)?,
            Commands::AI | Commands::RETRY => self.command_ai(file_index, block_index)?,
            Commands::PREV => self.command_prev_next(file_index, block_index, false)?,
            Commands::NEXT => self.command_prev_next(file_index, block_index, true)?,
            Commands::EDIT => self.command_edit(file_index, block_index)?,
            Commands::REMOVE => self.command_remove(file_index, block_index)?,
            Commands::UNDO => self.command_undo(file_index, block_index)?,
            _ => Err("Unknown command".to_string())?,
        }
        Ok(())
    }

    fn command_ack(&self, file_index: usize, block_index: usize) -> Result<(), NoCommit> {
        let change = self.blocks_mut(file_index)?.command_ack(block_index);
        if let Some(change) = change {
            self.set_modified();
            self.frontend.file_changed(file_index);
            self.frontend.view_changed(file_index, change);
        }
        Ok(())
    }

    /// Ack all clean hunks in the current commit. Does nothing if there's no current commit
    /// or if there are no clean unacked hunks.
    pub fn command_ack_all_clean(&self) {
        let changes = {
            let mut changes = vec![];
            let Ok(mut commit) = self.commit_mut() else { return };
            for file_index in 0 .. commit.files.len() {
                let blocks = commit.files[file_index].blocks.as_mut().unwrap();
                for block_index in 0 .. blocks.len() {
                    let modification = if self.overrides >= Overrides::AllClean {
                        blocks.command_ack(block_index)
                    } else {
                        blocks.command_ack_if_clean(block_index)
                    };
                    if let Some(modification) = modification {
                        changes.push((file_index, modification));
                    }
                }
            }
            changes
        };
        if changes.is_empty() {
            return;
        }
        self.set_modified();
        let mut last_file = None;
        for (file_index, modification) in changes {
            if Some(file_index) != last_file {
                self.frontend.file_changed(file_index);
            }
            last_file = Some(file_index);
            self.frontend.view_changed(file_index, modification);
        }
    }

    fn command_ai(&self, file_index: usize, block_index: usize) -> Result<(), NoCommit> {
        let change = {
            let mut rpc = self.rpc_mut();
            let mut blocks = self.blocks_mut(file_index)?;
            let block = blocks.get(block_index);
            let target = self.clone();
            let unique_id = block.unique_id;
            rpc.queue_ai(
                blocks.old_lines_slice(block_index),
                &block.as_change().hunk,
                move |r| target.populate_ai_result(unique_id, false, r),
                self.frontend.clone(),
            );
            rpc.execute(self.frontend.clone());
            blocks.set_change_interim(block_index)
        };
        self.frontend.view_changed(file_index, change);
        Ok(())
    }

    fn command_prev_next(&self, file_index: usize, block_index: usize, next: bool)
        -> Result<(), NoCommit>
    {
        let change = self.blocks_mut(file_index)?.command_prev_next(block_index, next);
        self.frontend.view_changed(file_index, change);
        Ok(())
    }

    fn command_edit(&self, file_index: usize, block_index: usize) -> Result<(), NoCommit> {
        let (data, unique_id, change) = self.blocks_mut(file_index)?
            .set_change_editing(block_index);
        let target = self.clone();
        self.frontend.run_editor(
            // editor_filename cannot return Err, since we already got the blocks before
            self.editor_filename(file_index).unwrap(),
            data,
            Box::new(move |data| target.edit_done(unique_id, data)),
        );
        self.frontend.view_changed(file_index, change);
        Ok(())
    }

    /// If the `unique_id` does not refer to the current commit, do nothing and just drop the
    /// `data`.
    fn edit_done(&self, unique_id: UniqueId, data: Option<Vec<u8>>) {
        let (file_index, block_index) = match self.resolve_unique_id(unique_id) {
            Some(indices) => indices,
            None => return
        };
        let was_edited = data.is_some();
        let change = {
            // blocks_mut cannot return None, since we got the index from resolve_unique_id
            let mut blocks = self.blocks_mut(file_index).unwrap();
            if let Some(data) = data {
                self.set_modified();
                blocks.command_edited(block_index, data.into())
            } else {
                blocks.clear_change_editing(block_index)
            }
        };
        if was_edited {
            self.frontend.file_changed(file_index);
        }
        self.frontend.view_changed(file_index, change);
    }

    fn command_remove(&self, file_index: usize, block_index: usize) -> Result<(), NoCommit> {
        self.set_modified();
        let change = self.blocks_mut(file_index)?.command_unapply(block_index);
        self.frontend.file_changed(file_index);
        self.frontend.view_changed(file_index, change);
        Ok(())
    }

    fn command_undo(&self, file_index: usize, block_index: usize) -> Result<(), String> {
        let mut blocks = self.blocks_mut(file_index)?;
        let undo = blocks.pop_undo(block_index).unwrap();
        let change = undo.undo(&mut blocks, block_index)
            .ok_or_else(||
                "Cannot undo. Other operation modified the original location.".to_string()
            )?;
        drop(blocks);
        self.frontend.file_changed(file_index);
        self.frontend.view_changed(file_index, change);
        Ok(())
    }

    fn submit_telemetry(&self) {
        let data = self.telemetry.take();
        // After the first submission, we want to explicitly track if we committed or not.
        self.telemetry.tele_set("committed", false);
        if self.overrides >= Overrides::NoTelemetry {
            #[cfg(feature = "debug_telemetry")]
            println!("telemetry: NoTelemetry is set, not submitting");
            return;
        }
        // If telemetry_agreement is communicating with the user (e.g. presents a dialog box),
        // we must not exit until that completes.
        let workspace = self.clone();
        self.spawn_blocking_task(async move {
            if let Some(id) = workspace.frontend.telemetry_agreement().await {
                let frontend = workspace.frontend.clone();
                workspace.spawn_terminable_task(data.submit(id, frontend));
            } else {
                #[cfg(feature = "debug_telemetry")]
                data.print_data();
            }
        });
    }

    /// Stop the UI, wait for tasks to finish (cancelling the background tasks after a
    /// timeout) and then terminate the app. This will happen in background; this method
    /// itself does not block and returns immediately.
    fn terminate(&self, exit_code: u8) {
        if self.terminating.get() {
            return;
        }
        self.terminating.set(true);
        self.set_complete_telemetry_stats();
        self.submit_telemetry();
        let workspace = self.clone();
        self.frontend.spawn_future(async move {
            workspace.background_tasks.finish().await;
            workspace.frontend.terminate(exit_code);
        });
    }

    /// Ask to terminate without saving the work. See `terminate` for more details.
    pub fn abort(&self) {
        self.terminate(1);
    }

    /// Save, stage and commit files as appropriate according to `store_mode`. Then
    /// terminate the app. See `terminate` for details.
    pub fn save(&self) -> Result<(), String> {
        let mut filesystem = RealFilesystem::default();
        for commit in self.commits.iter() {
            match self.store_mode.get() {
                StoreMode::Save => {
                    self.write_files(&mut filesystem, &commit)?;
                }
                StoreMode::Stage(_) => {
                    let names = self.write_files(&mut filesystem, &commit)?;
                    self.stage_files(&names)?;
                }
                StoreMode::Commit => {
                    let names = self.write_files(&mut filesystem, &commit)?;
                    self.stage_files(&names)?;
                    self.commit_files(&commit)?;
                }
            }
        }
        self.clear_modified();
        self.telemetry.tele_set("committed", true);
        self.terminate(0);
        Ok(())
    }

    /// Terminate without saving the work but indicate success. This is used when the frontend
    /// runs in a mode that does something else with the commit data and it succeeded.
    pub fn dont_save(&self) {
        self.clear_modified();
        self.terminate(0);
    }
}

// Must implement the Clone manually. derive(Clone) is not smart enough to recognize
// FE is inside Rc<> and thus doesn't have to be Clone.
impl<FE: Frontend + 'static> Clone for Workspace<FE> {
    fn clone(&self) -> Self {
        Workspace(self.0.clone())
    }
}

impl<FE: Frontend + 'static> Deref for Workspace<FE> {
    type Target = WorkspaceInner<FE>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<FE: Frontend + 'static> WorkspaceInner<FE> {
    /// Get the number of commits in the workspace.
    pub fn commit_count(&self) -> usize {
        self.commits.len()
    }

    /// Return true if the current commit is the last one.
    pub fn is_last_commit(&self) -> bool {
        !self.current_commit_index.get().next().within(self.commits.len())
    }

    /// Get a reference to the currently active commit. Return `Err(NoCommit)` if there is no
    /// active commit.
    pub fn commit(&self) -> Result<Ref<'_, Commit>, NoCommit> {
        self.commits.get(self.current_commit_index.get())
    }

    /// Get a mutable reference to the currently active commit. Return `Err(NoCommit)` if
    /// there is no active commit.
    fn commit_mut(&self) -> Result<RefMut<'_, Commit>, NoCommit> {
        self.commits.get_mut(self.current_commit_index.get())
    }

    /// Get a reference to the blocks in file with `file_index` in the current commit. Return
    /// `Err(NoCommit)` if there is no active commit.
    pub fn blocks(&self, file_index: usize) -> Result<Ref<'_, Blocks>, NoCommit> {
        Ok(Ref::map(
            self.commit()?,
            |commit| commit.files[file_index].blocks.as_ref().unwrap(),
        ))
    }

    /// Get a mutable reference to the blocks in file with `file_index` in the current commit.
    /// Return `Err(NoCommit)` if there is no active commit.
    fn blocks_mut(&self, file_index: usize) -> Result<RefMut<'_, Blocks>, NoCommit> {
        Ok(RefMut::map(
            self.commit_mut()?,
            |commit| commit.files[file_index].blocks.as_mut().unwrap(),
        ))
    }

    /// Execute the given closure for the blocks in every file in the current commit.
    pub fn for_each_blocks<F: FnMut(&Blocks)>(&self, f: F) {
        if let Ok(commit) = self.commit() {
            commit.files.iter().map(|file| file.blocks.as_ref().unwrap()).for_each(f);
        }
    }

    /// Get a reference to the block with `block_index` in file with `file_index` in the
    /// current commit. Return `Err(NoCommit)` if there is no active commit.
    pub fn block(&self, file_index: usize, block_index: usize)
        -> Result<Ref<'_, Block>, NoCommit>
    {
        Ok(Ref::map(
            self.commit()?,
            |commit| commit.files[file_index].blocks.as_ref().unwrap().get(block_index),
        ))
    }

    /// Get a mutable reference to the block with `block_index` in file with `file_index` in
    /// the current commit. Return `Err(NoCommit)` if there is no active commit.
    fn block_mut(&self, file_index: usize, block_index: usize)
        -> Result<RefMut<'_, Block>, NoCommit>
    {
        Ok(RefMut::map(
            self.commit_mut()?,
            |commit| commit.files[file_index].blocks.as_mut().unwrap().get_mut(block_index),
        ))
    }

    /// Execute the given closure for each block in each file in the current commit.
    pub fn for_each_block<F: FnMut(&Block)>(&self, f: F) {
        if let Ok(commit) = self.commit() {
            commit.files.iter()
                .map(|file| file.blocks.as_ref().unwrap().iter())
                .flatten()
                .for_each(f)
        }
    }

    fn rpc_mut(&self) -> RefMut<'_, Rpc> {
        self.rpc.borrow_mut()
    }

    pub fn telemetry(&self) -> &Telemetry {
        &self.telemetry
    }

    /// Return true if there were changes done to the workspace by the user or by a finished
    /// AI inference.
    pub fn modified(&self) -> bool {
        self.modified.get()
    }

    fn set_modified(&self) {
        self.modified.set(true)
    }

    /// Clears the `modified` indicator. Future changes by the user will set the indicator
    /// again.
    pub fn clear_modified(&self) {
        self.modified.set(false)
    }

    fn ensure_downstream_clean(&self) -> Result<(), String> {
        if self.downstream_repo.is_clean().map_err(|e| e.to_string())? {
            Ok(())
        } else {
            Err("The repository is not clean. Please commit or stash your changes first."
                .to_owned())
        }
    }

    fn editor_filename(&self, file_index: usize) -> Result<OsString, NoCommit> {
        Ok(OsStr::from_bytes(&self.blocks(file_index)?.old_lines.path).to_os_string())
    }

    /// Return true if the current mode allows multiple commits. It doesn't necessarily mean
    /// there are currently multiple commits.
    pub fn multicommit_capable(&self) -> bool {
        match &self.store_mode.get() {
            StoreMode::Commit => true,
            StoreMode::Stage(_) => false,
            StoreMode::Save => false,
        }
    }

    fn commit_title_for(&self, commit: Result<Ref<'_, Commit>, NoCommit>, prefix: &str)
        -> Formatted
    {
        let space = if prefix.len() > 0 { " " } else { "" };
        let prefix2 = match &self.store_mode.get() {
            StoreMode::Commit => "",
            StoreMode::Stage(merge_op) => match merge_op {
                MergeOp::None => "merge conflict",
                MergeOp::CherryPick => "cherry-picking",
                MergeOp::Rebase => "rebasing",
            }
            StoreMode::Save => "mergetool",
        };
        let space2 = if prefix2.len() > 0 { " " } else { "" };
        let Ok(description) = commit
            .and_then(|commit|
                if commit.has_metadata() { Ok(commit) } else { Err(NoCommit::default()) }
            )
            .map(|commit| commit.description())
        else {
            // no commit or no commit metadata
            let prefix2 = if prefix2.len() > 0 { prefix2 } else { "nothing to do" };
            return format!("{prefix}{space}{prefix2}").into();
        };
        let mut title: Formatted = format!(
            "{prefix}{space}{prefix2}{space2}{} ",
            (&description.id[.. 12]).as_bstr(),
        ).into();
        title.push_style(&description.subject, Style::Italic);
        title
    }

    pub fn commit_title(&self, prefix: &str) -> Formatted {
        self.commit_title_for(self.commit(), prefix)
    }

    /// Get the formatted title for the commit at the given index.
    pub fn commit_title_at(&self, index: usize) -> Formatted {
        self.commit_title_for(self.commits.get(index.into()), "")
    }

    /// Return the descriptions of all commits together with the index of the current commit.
    /// Return `None` if commits don't have metadata or there is no current commit.
    pub fn commit_descriptions(&self) -> Option<(Vec<Rc<git::Description>>, usize)> {
        if !self.commit().ok()?.has_metadata() {
            return None;
        }
        Some((
            self.commits.iter()
                .map(|c| c.description())
                .collect(),
            self.current_commit_index.get().index(),
        ))
    }

    /// Find the index to which the given commit should be inserted chronologically.
    pub fn target_commit_position(&self, commit: &git::Description) -> usize {
        self.commits.iter()
            .position(|c| c.description().as_ref() >= commit)
            .unwrap_or(self.commits.len())
    }

    /// Return true if the given commit is among the commits that are applied (i.e. before,
    /// and including, the current commit).
    fn commit_already_applied(&self, commit: &git::Description) -> bool {
        let Some(index) = self.current_commit_index.get().try_index() else { return false; };
        self.commits.iter().take(index + 1).any(|c| c.description().as_ref() == commit)
    }

    /// Find the (file_index, block_index) that corresponds to the unique id. If the change
    /// with the unique_id is no longer present, return None.
    pub fn resolve_unique_id(&self, unique_id: UniqueId) -> Option<(usize, usize)> {
        for (file_index, file) in self.commit().ok()?.files.iter().enumerate() {
            for (block_index, block) in file.blocks.as_ref()?.iter().enumerate() {
                if block.unique_id == unique_id {
                    return Some((file_index, block_index));
                }
            }
        }
        None
    }

    /// Get the unique_id for the change at the given file_index and block_index. Return
    /// `Err(NoCommit)` if there is no active commit.
    pub fn get_unique_id(&self, file_index: usize, block_index: usize)
        -> Result<UniqueId, NoCommit>
    {
        Ok(self.block(file_index, block_index)?.unique_id)
    }

    /// Find the next (or previous, if `backwards` is true) block that's unacked in the
    /// current commit. `file_index` and `block_index` is the current block. Returns `None`
    /// if there are no more unacked blocks, or `Some(file_index, block_index)`.
    pub fn find_next_unacked(
        &self,
        file_index: usize,
        block_index: usize,
        backwards: bool,
    ) -> Option<(usize, usize)> {
        let file_start = file_index as isize;
        let block_start = block_index as isize;
        let commit = self.commit().ok()?;
        let files = &commit.files;
        let step = if backwards { -1 } else { 1 };
        let mut file_i = file_start;
        let mut block_i = block_start;
        loop {
            block_i += step;
            while
                block_i < 0 ||
                block_i >= files[file_i as usize].blocks.as_ref().unwrap().len() as isize
            {
                file_i += step;
                if file_i == files.len() as isize {
                    file_i = 0;
                } else if file_i < 0 {
                    file_i = files.len() as isize - 1;
                }
                block_i = if backwards {
                    files[file_i as usize].blocks.as_ref().unwrap().len() as isize - 1
                } else {
                    0
                };
                if file_i == file_start && block_i == block_start {
                    // prevent starting a second complete cycle
                    break;
                }
            }
            if file_i == file_start && block_i == block_start {
                // searched everything
                return None;
            }
            if files[file_i as usize].blocks.as_ref().unwrap()
                .get(block_i as usize).is_unacked()
            {
                return Some((file_i as usize, block_i as usize));
            }
        }
    }

    fn set_or_toggle_fold_block(
        &self,
        file_index: usize,
        block_index: usize,
        allow_shrink: bool,
        allow_expand: bool,
    ) {
        if let Some(change) = self.blocks_mut(file_index)
            .ok()
            .and_then(|mut blocks|
                blocks.set_or_toggle_fold(block_index, allow_shrink, allow_expand)
            )
        {
            self.frontend.view_changed(file_index, change);
        }
    }

    /// Folds/unfolds the block identified by `file_index` and `block_index`. Does nothing
    /// if there's no current commit.
    pub fn set_fold_block(&self, file_index: usize, block_index: usize, folded: bool) {
        self.set_or_toggle_fold_block(file_index, block_index, folded, !folded);
    }

    /// Toggles the fold state of the block identified by `file_index` and `block_index`. Does
    /// nothing if there's no current commit.
    pub fn toggle_fold_block(&self, file_index: usize, block_index: usize) {
        self.set_or_toggle_fold_block(file_index, block_index, true, true);
    }

    /// If the `unique_id` does not refer to the current commit, do nothing and just drop the
    /// `result`.
    fn populate_ai_result(
        &self,
        unique_id: UniqueId,
        no_undo: bool,
        result: Result<(Vec<Lines>, Vec<u8>), String>
    ) {
        let (file_index, block_index) = match self.resolve_unique_id(unique_id) {
            Some(indices) => indices,
            None => return
        };
        self.set_modified();
        let change = {
            // blocks_mut cannot return None, since we got the index from resolve_unique_id
            let mut blocks = self.blocks_mut(file_index).unwrap();
            match result {
                Err(msg) => blocks.set_change_failed(block_index, msg),
                Ok((lines, confidence)) =>
                    blocks.command_ai(block_index, lines, confidence, no_undo),
            }
        };
        self.frontend.file_changed(file_index);
        self.frontend.view_changed(file_index, change);
    }

    /// Assumes the caller ensured the file_index refers to the current commit and that
    /// there's a current commit.
    fn suggestions_updated(&self, file_index: usize, block_index: usize) {
        let change = self.blocks(file_index).unwrap().suggestion_range(block_index);
        self.frontend.view_changed(file_index, change);
    }

    /// Get the number of blocks that have not been approved in the current commit.
    pub fn count_unacked(&self) -> usize {
        let mut sum = 0;
        self.for_each_blocks(|blocks| sum += blocks.count_unacked());
        sum
    }

    /// Get the number of blocks that have not been approved in the given file in the current
    /// commit.
    pub fn count_file_unacked(&self, file_index: usize) -> usize {
        self.blocks(file_index).unwrap().count_unacked()
    }

    /// Get the number of blocks that have not been approved and that are clean cherry-picks
    /// in the current commit.
    pub fn count_unacked_clean(&self) -> usize {
        let mut sum = 0;
        self.for_each_blocks(|blocks| sum += blocks.count_unacked_clean());
        sum
    }

    /// Return true if the current commit is fully resolved and we can move to the next one
    /// (or commit if this is the last commit).
    pub fn is_resolved(&self) -> bool {
        self.count_unacked() == 0
    }

    /// Return true if the current commit was a clean cherry-pick. Panics if there's no
    /// current commit or if the current commit is not applied.
    pub fn is_clean_cherry_pick(&self) -> bool {
        if self.overrides >= Overrides::AllClean {
            return true;
        }
        self.commit().unwrap().is_clean_cherry_pick().unwrap()
    }

    fn set_basic_telemetry_stats(&self) {
        self.telemetry.tele_set("commits", self.commits.len());
        let file_count: usize = self.commits.iter().map(|commit| commit.file_count()).sum();
        self.telemetry.tele_set("files", file_count);
    }

    fn set_complete_telemetry_stats(&self) {
        self.set_basic_telemetry_stats();
        let (mut commits_applied, mut commits_clean) = (0, 0);
        let mut files_applied = 0;
        let (mut hunks_clean, mut hunks_fuzz, mut hunks_conflict) = (0, 0, 0);
        let mut hunks_unapplied = 0;
        let mut file_types = HashSet::new();
        for commit in self.commits.iter() {
            match commit.is_clean_cherry_pick() {
                Some(true) => commits_clean += 1,
                Some(false) => {}
                None => continue
            }
            // the match above filtered us to applied commits only
            commits_applied += 1;
            for blocks in commit.files.iter().map(|file| file.blocks.as_ref().unwrap()) {
                for block in blocks.iter() {
                    if let Some(change) = block.try_as_change() {
                        match change.fuzz {
                            apply::AppliedFuzz::None => hunks_clean += 1,
                            apply::AppliedFuzz::Context => hunks_fuzz += 1,
                            apply::AppliedFuzz::Content => hunks_conflict += 1,
                        }
                    }
                    if block.is_unapplied() {
                        hunks_unapplied += 1;
                    }
                }
                file_types.insert(blocks.old_lines.file_type);
                files_applied += 1;
            }
        }
        self.telemetry.tele_set("commits_applied", commits_applied);
        self.telemetry.tele_set("commits_clean", commits_clean);
        self.telemetry.tele_set("files_applied", files_applied);
        self.telemetry.tele_set("hunks_clean", hunks_clean);
        self.telemetry.tele_set("hunks_fuzz", hunks_fuzz);
        self.telemetry.tele_set("hunks_conflict", hunks_conflict);
        self.telemetry.tele_set("hunks_unapplied", hunks_unapplied);
        self.telemetry.tele_set("file_types", file_types.into_iter().collect::<Vec<_>>());
    }

    /// Returns up to two paths that must be added to the git index. Panics if the commit
    /// the file belongs to is not applied. Panics if some hunks are not resolved (AI or
    /// edit is in progress or failed).
    fn write_file<F: FilesystemWrite>(&self, file: &CommitFile, filesystem: &mut F)
        -> Result<(Option<OsString>, Option<OsString>), String>
    {
        let blocks = file.blocks.as_ref().unwrap();
        let mut data = blocks.iter_bytes().peekable();
        let mut file_path = OsStr::from_bytes(&blocks.old_lines.path);
        let file_exists = filesystem.exists(file_path).map_err(|e| e.to_string())?;
        let (permissions, rename_to) = match &file.upstream_metadata.op {
            diff_meta::Op::Added => {
                if file_exists {
                    // We're adding a file that's already there. Preserve the current
                    // permissions. Ignore the permissions in the patch. The file got likely
                    // created by an out of order backport that changed the file type.
                    // (FIXME: handle this better in the future.)
                    (None, None)
                } else {
                    (Some(file.upstream_metadata.mode.to_permissions().unwrap()), None)
                }
            }
            diff_meta::Op::Removed => {
                if data.peek().is_none() {
                    return Ok((
                        if file_exists {
                            filesystem.remove_file(file_path).map_err(|e| e.to_string())?;
                            Some(file_path.to_os_string())
                        } else {
                            // Deleting a file that does not exist. This is a no-op.
                            None
                        },
                        None
                    ));
                }
                // We have something to write. Do not delete the file.
                (None, None)
            }
            diff_meta::Op::Modified { new_name, new_mode } => {
                // If the file does not exist in the target project and no changes are being
                // made to it, return (None, None)
                if !file_exists && data.peek().is_none() {
                    return Ok((None, None));
                }
                // The file may have already been renamed downstream. Do not rename in such
                // case.
                let mut rename_to = if let Some(dest) = new_name {
                    let dest = OsStr::from_bytes(&dest);
                    if file_path != dest { Some(dest) } else { None }
                } else {
                    None
                };
                // If we are renaming but the source file does not exist, write directly
                // to the destination file.
                if !file_exists && rename_to.is_some() {
                    file_path = rename_to.take().unwrap();
                }
                if let Some(perm) = new_mode.to_permissions() {
                    (Some(perm.clone()), rename_to)
                } else {
                    (None, rename_to)
                }
            }
        };
        filesystem.create_file(file_path, rename_to, permissions, data)
            .map_err(|e| e.to_string())?;
        Ok((
            Some(file_path.to_os_string()),
            rename_to.map(|path| path.to_os_string()),
        ))
    }

    /// Returns the paths that must be added to the git index by calling `stage_files`.
    fn write_files<F: FilesystemWrite>(&self, filesystem: &mut F, commit: &Commit)
        -> Result<Vec<OsString>, String>
    {
        let mut paths: Vec<OsString> = Vec::with_capacity(commit.files.len());
        let mut errs: Vec<String> = vec![];
        for file in commit.files.iter() {
            match self.write_file(file, filesystem) {
                Ok((None, None)) => (),
                Ok((Some(path), None)) => paths.push(path),
                Ok((None, Some(path))) => paths.push(path),
                Ok((Some(path1), Some(path2))) => {
                    paths.push(path1);
                    paths.push(path2);
                }
                Err(err) => errs.push(err)
            }
        }
        if errs.is_empty() {
            Ok(paths)
        } else {
            Err(errs.join(" "))
        }
    }

    fn stage_files(&self, files: &[OsString]) -> Result<(), String> {
        self.downstream_repo.add_to_index(files.iter().map(|v| v.as_os_str()))
            .map_err(|err| err.to_string())?;
        Ok(())
    }

    /// Create a git commit. Must call `write_files` and `stage_files` first.
    fn commit_files(&self, commit: &Commit) -> Result<(), String> {
        if !self.downstream_repo.is_staged().map_err(|err| err.to_string())? {
            return Ok(());
        }
        let upstream_desc = commit.description();
        let mut message = vec![];
        message.extend_from_slice(&upstream_desc.subject);
        if !self.commit_headers.is_empty() {
            message.push(b'\n');
            for header in self.commit_headers.iter() {
                message.push(b'\n');
                message.extend_from_slice(header.as_bytes());
            }
        }
        message.extend_from_slice(b"\n\ncommit ");
        message.extend_from_slice(&upstream_desc.id);
        message.extend_from_slice(b"\nAuthor: ");
        message.extend_from_slice(&upstream_desc.author);
        message.extend_from_slice(b"\nDate:   ");
        message.extend_from_slice(&upstream_desc.author_date);
        message.extend_from_slice(b"\n\n    ");
        message.extend_from_slice(&upstream_desc.subject);
        message.extend_from_slice(b"\n\n");
        for line in upstream_desc.body.split_inclusive(|&b| b == b'\n') {
            message.extend_from_slice(b"    ");
            message.extend_from_slice(line);
        }
        let conflicts = commit.conflicts();
        if conflicts.len() > 0 {
            message.extend_from_slice(b"\nConflicts:\n");
            for conflict in conflicts.iter() {
                message.extend_from_slice(b"- ");
                message.extend_from_slice(&conflict.path);
                message.push(b':');
                if conflict.problems.len() == 1 {
                    // keep everything on a single line
                    message.push(b' ');
                    message.extend_from_slice(&conflict.problems[0]);
                } else {
                    // multi-line output
                    for problem in conflict.problems.iter() {
                        message.extend_from_slice(b"\n  - ");
                        message.extend_from_slice(problem);
                    }
                }
                message.push(b'\n');
            }
        }
        message.extend_from_slice(b"\n(cherry picked from commit ");
        message.extend_from_slice(&upstream_desc.id);
        message.push(b')');
        if commit.was_ai_used() {
            message.extend_from_slice(b"\nAssisted-by: Patchpal AI\n");
        } else {
            message.extend_from_slice(b"\nAssisted-by: Patchpal\n");
        }
        self.downstream_repo.commit_index(&message).map_err(|err| err.to_string())
    }

    /// Start the given future. While the UI is running, the task is not limited. If
    /// the task is still running after the UI finishes, it is given a timeout, after
    /// which it is aborted.
    pub(crate) fn spawn_terminable_task(&self, future: impl Future<Output = ()> + 'static) {
        self.background_tasks.push_non_blocking(future);
    }

    /// Start the given future. Wait for it to complete, even when the UI is finished.
    pub(crate) fn spawn_blocking_task(&self, future: impl Future<Output = ()> + 'static) {
        self.background_tasks.push_blocking(future);
    }
}

impl<FE: Frontend + 'static> history::Consumer<UniqueId> for Workspace<FE> {
    fn received_missing_upstream(&self, _unique_id: UniqueId, _commit: git::Description) {
        // ignore for now
    }

    /// If the `unique_id` does not refer to the current commit, do nothing and just drop the
    /// `desc`.
    fn received_missing_downstream(&self, unique_id: UniqueId, desc: git::Description) {
        let res = self.resolve_unique_id(unique_id);
        let (file_index, block_index) = match res {
            Some(indices) => indices,
            None => return
        };
        // The search does not take into account the commits we applied ourselves. If a hunk
        // still doesn't apply cleanly with a dependency included (e.g. because we're also
        // missing something else), that dependency will still be returned from the search.
        // Filter it out.
        if self.commit_already_applied(&desc) {
            return;
        }
        // blocks_mut cannot return None, since we got the index from resolve_unique_id
        self.block_mut(file_index, block_index).unwrap().as_change_mut()
            .missing.push(Rc::new(desc));
        self.suggestions_updated(file_index, block_index);
    }

    /// If the `unique_id` does not refer to the current commit, do nothing and just drop the
    /// `result`.
    fn received_missing_result(&self, unique_id: UniqueId, result: Result<(), String>) {
        let res = self.resolve_unique_id(unique_id);
        let (file_index, block_index) = match res {
            Some(indices) => indices,
            None => return
        };
        // blocks_mut cannot return None, since we got the index from resolve_unique_id
        self.block_mut(file_index, block_index).unwrap()
            .set_suggestion_search_state(match result {
                Ok(()) => SuggestionSearchState::Done,
                Err(msg) => SuggestionSearchState::Err(msg),
            });
        self.suggestions_updated(file_index, block_index);
    }
}
