// Copyright 2020 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::io::Write as _;
use std::sync::Arc;

use futures::TryStreamExt as _;
use jj_lib::backend::CommitId;
use jj_lib::commit::Commit;
use jj_lib::repo::Repo as _;
use jj_lib::revset::ResolvedRevsetExpression;
use jj_lib::revset::RevsetExpression;
use jj_lib::revset::RevsetFilterPredicate;
use jj_lib::revset::RevsetStreamExt as _;

use crate::cli_util::CommandHelper;
use crate::cli_util::WorkspaceCommandHelper;
use crate::cli_util::short_commit_hash;
use crate::command_error::CommandError;
use crate::command_error::user_error;
use crate::ui::Ui;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct MovementArgs {
    pub offset: u64,
    pub edit: bool,
    pub no_edit: bool,
    pub conflict: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct MovementArgsInternal {
    offset: u64,
    should_edit: bool,
    conflict: bool,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Direction {
    Next,
    Prev,
}

impl Direction {
    fn cmd(&self) -> &'static str {
        match self {
            Self::Next => "next",
            Self::Prev => "prev",
        }
    }

    fn target_not_found_error(
        &self,
        workspace_command: &WorkspaceCommandHelper,
        args: &MovementArgsInternal,
        commits: &[Commit],
    ) -> CommandError {
        let offset = args.offset;
        let err_msg = match (self, args.should_edit, args.conflict) {
            // in edit mode, start_revset is the WC, so we only look for direct descendants.
            (Self::Next, true, true) => {
                String::from("The working copy has no descendants with conflicts")
            }
            (Self::Next, true, false) => {
                format!("No descendant found {offset} commit(s) forward from the working copy",)
            }
            // in non-edit mode, start_revset is the parent of WC, so we look for other descendants
            // of start_revset.
            (Self::Next, false, true) => {
                String::from("The working copy parent(s) have no other descendants with conflicts")
            }
            (Self::Next, false, false) => format!(
                "No other descendant found {offset} commit(s) forward from the working copy \
                 parent(s)",
            ),
            // The WC can never be an ancestor of the start_revset since start_revset is either
            // itself or it's parent.
            (Self::Prev, true, true) => {
                String::from("The working copy has no ancestors with conflicts")
            }
            (Self::Prev, true, false) => {
                format!("No ancestor found {offset} commit(s) back from the working copy",)
            }
            (Self::Prev, false, true) => {
                String::from("The working copy parent(s) have no ancestors with conflicts")
            }
            (Self::Prev, false, false) => format!(
                "No ancestor found {offset} commit(s) back from the working copy parents(s)",
            ),
        };

        let template = workspace_command.commit_summary_template();
        let mut cmd_err = user_error(err_msg);
        for commit in commits {
            cmd_err.add_formatted_hint_with(|formatter| {
                if args.should_edit {
                    write!(formatter, "Working copy: ")?;
                } else {
                    write!(formatter, "Working copy parent: ")?;
                }
                template.format(commit, formatter)
            });
        }

        cmd_err
    }

    fn build_target_revset(
        &self,
        working_revset: &Arc<ResolvedRevsetExpression>,
        start_revset: &Arc<ResolvedRevsetExpression>,
        args: &MovementArgsInternal,
    ) -> Result<Arc<ResolvedRevsetExpression>, CommandError> {
        let nth = match (self, args.should_edit) {
            (Self::Next, true) => start_revset.descendants_at(args.offset),
            (Self::Next, false) => start_revset
                .children()
                .minus(working_revset)
                .descendants_at(args.offset - 1),
            (Self::Prev, _) => start_revset.ancestors_at(args.offset),
        };

        let target_revset = match (self, args.conflict) {
            (_, false) => nth,
            (Self::Next, true) => nth
                .descendants()
                .filtered(RevsetFilterPredicate::HasConflict)
                .roots(),
            // If people desire to move to the root conflict, replace the `heads()` below
            // with `roots(). But let's wait for feedback.
            (Self::Prev, true) => nth
                .ancestors()
                .filtered(RevsetFilterPredicate::HasConflict)
                .heads(),
        };

        Ok(target_revset)
    }
}

async fn get_target_commit(
    ui: &mut Ui,
    workspace_command: &WorkspaceCommandHelper,
    direction: Direction,
    working_commit_id: &CommitId,
    args: &MovementArgsInternal,
) -> Result<Commit, CommandError> {
    let wc_revset = RevsetExpression::commit(working_commit_id.clone());

    // If we're not editing, the working-copy shouldn't have any children
    if !args.should_edit
        && !workspace_command
            .repo()
            .view()
            .heads()
            .contains(working_commit_id)
    {
        return Err(user_error("The working copy must not have any children")
            .hinted("Create a new commit on top of this one or use `--edit`"));
    }

    // If we're editing, start at the working-copy commit. Otherwise, start from
    // its direct parent(s).
    let start_revset = if args.should_edit {
        wc_revset.clone()
    } else {
        wc_revset.parents()
    };

    let target_revset = direction.build_target_revset(&wc_revset, &start_revset, args)?;

    let targets: Vec<Commit> = target_revset
        .evaluate(workspace_command.repo().as_ref())?
        .stream()
        .commits(workspace_command.repo().store())
        .try_collect()
        .await?;

    let target = match targets.as_slice() {
        [target] => target,
        [] => {
            // We found no ancestor/descendant.
            let start_commits: Vec<Commit> = start_revset
                .evaluate(workspace_command.repo().as_ref())?
                .stream()
                .commits(workspace_command.repo().store())
                .try_collect()
                .await?;
            return Err(direction.target_not_found_error(workspace_command, args, &start_commits));
        }
        commits => choose_commit(ui, workspace_command, direction, commits)?,
    };

    Ok(target.clone())
}

fn choose_commit<'a>(
    ui: &Ui,
    workspace_command: &WorkspaceCommandHelper,
    direction: Direction,
    commits: &'a [Commit],
) -> Result<&'a Commit, CommandError> {
    writeln!(
        ui.stderr(),
        "ambiguous {} commit, choose one to target:",
        direction.cmd()
    )?;
    let mut formatter = ui.stderr_formatter();
    let template = workspace_command.commit_summary_template();
    let mut choices: Vec<String> = Default::default();
    for (i, commit) in commits.iter().enumerate() {
        write!(formatter, "{}: ", i + 1)?;
        template.format(commit, formatter.as_mut())?;
        writeln!(formatter)?;
        choices.push(format!("{}", i + 1));
    }
    writeln!(formatter, "q: quit the prompt")?;
    choices.push("q".to_string());
    drop(formatter);

    let index = ui.prompt_choice(
        "enter the index of the commit you want to target",
        &choices,
        None,
    )?;
    commits
        .get(index)
        .ok_or_else(|| user_error("ambiguous target commit"))
}

pub(crate) async fn move_to_commit(
    ui: &mut Ui,
    command: &CommandHelper,
    direction: Direction,
    args: &MovementArgs,
) -> Result<(), CommandError> {
    let mut workspace_command = command.workspace_helper(ui)?;

    let current_wc_id = workspace_command
        .get_wc_commit_id()
        .ok_or_else(|| user_error("This command requires a working copy"))?;

    let config_edit_flag = workspace_command.settings().get_bool("ui.movement.edit")?;
    let args = MovementArgsInternal {
        should_edit: args.edit || (!args.no_edit && config_edit_flag),
        offset: args.offset,
        conflict: args.conflict,
    };

    let target = get_target_commit(ui, &workspace_command, direction, current_wc_id, &args).await?;
    let current_short = short_commit_hash(current_wc_id);
    let target_short = short_commit_hash(target.id());
    let cmd = direction.cmd();
    // We're editing, just move to the target commit.
    if args.should_edit {
        // We're editing, the target must be rewritable.
        workspace_command.check_rewritable([target.id()]).await?;
        let mut tx = workspace_command.start_transaction();
        tx.edit(&target)?;
        tx.finish(
            ui,
            format!("{cmd}: {current_short} -> editing {target_short}"),
        )
        .await?;
        return Ok(());
    }
    let mut tx = workspace_command.start_transaction();
    // Move the working-copy commit to the new parent.
    tx.check_out(&target)?;
    tx.finish(ui, format!("{cmd}: {current_short} -> {target_short}"))
        .await?;
    Ok(())
}
