// Copyright 2020-2023 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::slice;

use clap_complete::ArgValueCandidates;
use futures::StreamExt as _;
use futures::TryStreamExt as _;
use futures::stream;
use futures::stream::LocalBoxStream;
use jj_lib::graph::GraphEdge;
use jj_lib::graph::reverse_graph;
use jj_lib::op_walk;
use jj_lib::operation::Operation;
use jj_lib::repo::RepoLoader;

use super::diff::parse_op_diff_changes_in;
use super::diff::show_op_diff;
use crate::cli_util::CommandHelper;
use crate::cli_util::LogContentFormat;
use crate::cli_util::WorkspaceCommandEnvironment;
use crate::cli_util::format_template;
use crate::cli_util::merge_operations;
use crate::command_error::CommandError;
use crate::complete;
use crate::diff_util::DiffFormatArgs;
use crate::diff_util::DiffRenderer;
use crate::diff_util::diff_formats_for_log;
use crate::formatter::Formatter;
use crate::graphlog::GraphStyle;
use crate::graphlog::get_graphlog;
use crate::operation_templater::OperationTemplateLanguage;
use crate::templater::TemplateRenderer;
use crate::ui::Ui;

/// Show the operation log
///
/// Like other commands, `jj op log` snapshots the current working-copy changes
/// and reconciles divergent operations. Use `--at-op=@ --ignore-working-copy`
/// to inspect the current state without mutation.
#[derive(clap::Args, Clone, Debug)]
pub struct OperationLogArgs {
    /// Limit number of operations to show
    ///
    /// Applied after operations are reordered topologically, but before being
    /// reversed.
    #[arg(long, short = 'n')]
    limit: Option<usize>,

    /// Show operations in the opposite order (older operations first)
    #[arg(long)]
    reversed: bool,

    /// Don't show the graph, show a flat list of operations
    #[arg(long, short = 'G')]
    no_graph: bool,

    /// Render each operation using the given template
    ///
    /// You can specify arbitrary template expressions using the
    /// [built-in keywords]. See [`jj help -k templates`] for more
    /// information.
    ///
    /// [built-in keywords]:
    ///     https://docs.jj-vcs.dev/latest/templates/#operation-keywords
    ///
    /// [`jj help -k templates`]:
    ///     https://docs.jj-vcs.dev/latest/templates/
    #[arg(long, short = 'T')]
    #[arg(add = ArgValueCandidates::new(complete::template_aliases))]
    template: Option<String>,

    /// Show changes to the repository at each operation
    #[arg(long, short = 'd')]
    op_diff: bool,

    /// Show patch of modifications to changes (implies --op-diff)
    ///
    /// If the previous version has different parents, it will be temporarily
    /// rebased to the parents of the new version, so the diff is not
    /// contaminated by unrelated changes.
    #[arg(long, short = 'p')]
    patch: bool,

    #[command(flatten)]
    diff_format: DiffFormatArgs,

    /// Show only changed revisions matching the given revset expression
    ///
    /// If no revisions are specified, this defaults to the
    /// `revsets.op-diff-changes-in` setting.
    #[arg(long, value_name = "REVSETS")]
    show_changes_in: Option<String>,
}

pub async fn cmd_op_log(
    ui: &mut Ui,
    command: &CommandHelper,
    args: &OperationLogArgs,
) -> Result<(), CommandError> {
    if command.is_working_copy_writable() {
        let workspace_command = command.workspace_helper(ui).await?;
        let current_op = workspace_command.repo().operation();
        let repo_loader = workspace_command.workspace().repo_loader();
        do_op_log(ui, workspace_command.env(), repo_loader, current_op, args).await
    } else {
        // Don't load the repo so that the operation history can be inspected
        // even with a corrupted repo state. For example, you can find the first
        // bad operation id to be abandoned.
        let workspace = command.load_workspace()?;
        let workspace_env = command.workspace_environment(ui, &workspace)?;
        let repo_loader = workspace.repo_loader();
        let current_op =
            command.resolve_operation(ui, workspace.repo_loader(), workspace.workspace_name())?;
        do_op_log(ui, &workspace_env, repo_loader, &current_op, args).await
    }
}

async fn do_op_log(
    ui: &mut Ui,
    workspace_env: &WorkspaceCommandEnvironment,
    repo_loader: &RepoLoader,
    current_op: &Operation,
    args: &OperationLogArgs,
) -> Result<(), CommandError> {
    let settings = repo_loader.settings();
    let graph_style = GraphStyle::from_settings(settings)?;
    let with_content_format = LogContentFormat::new(ui, settings)?;

    let template: TemplateRenderer<Operation>;
    let op_node_template: TemplateRenderer<Operation>;
    {
        let language = OperationTemplateLanguage::new(
            repo_loader,
            Some(current_op.id()),
            workspace_env.cwd(),
            workspace_env.operation_template_extensions(),
        );
        let text = match &args.template {
            Some(value) => value.to_owned(),
            None => settings.get_string("templates.op_log")?,
        };
        template = workspace_env
            .parse_template(ui, &language, &text)?
            .labeled(["op_log", "operation"]);
        op_node_template = workspace_env
            .parse_template(
                ui,
                &language,
                &settings.get_string("templates.op_log_node")?,
            )?
            .labeled(["op_log", "operation", "node"]);
    }

    let diff_formats = diff_formats_for_log(settings, &args.diff_format, args.patch)?;
    let maybe_show_op_diff = if args.op_diff || !diff_formats.is_empty() {
        let template_text = settings.get_string("templates.commit_summary")?;
        let op_diff_changes_expr =
            parse_op_diff_changes_in(ui, settings, workspace_env, args.show_changes_in.as_deref())?;
        let show = async move |ui: &Ui,
                               formatter: &mut dyn Formatter,
                               op: &Operation,
                               with_content_format: &LogContentFormat| {
            let parent_ops = op.parents().await?;
            let workspace_name = None;
            let transaction_description = None;
            let command_args = [];
            let merged_parent_op = merge_operations(
                None,
                repo_loader,
                parent_ops.clone(),
                workspace_name,
                transaction_description,
                &command_args,
            )
            .await?;
            let parent_repo = repo_loader.load_at(&merged_parent_op).await?;
            let repo = repo_loader.load_at(op).await?;

            let id_prefix_context = workspace_env.new_id_prefix_context();
            let commit_summary_template = {
                let language =
                    workspace_env.commit_template_language(repo.as_ref(), &id_prefix_context);
                workspace_env
                    .parse_template(ui, &language, &template_text)?
                    .labeled(["op_log", "commit"])
            };
            let path_converter = workspace_env.path_converter();
            let conflict_marker_style = workspace_env.conflict_marker_style();
            let diff_renderer = (!diff_formats.is_empty()).then(|| {
                DiffRenderer::new(
                    repo.as_ref(),
                    path_converter,
                    conflict_marker_style,
                    diff_formats.clone(),
                )
            });

            // TODO: Merged repo may have newly rebased commits, which wouldn't
            // exist in the index. (#4465)
            if parent_ops.len() > 1 {
                return Ok(());
            }
            show_op_diff(
                ui,
                workspace_env,
                formatter,
                repo.as_ref(),
                &parent_repo,
                &repo,
                &commit_summary_template,
                (!args.no_graph).then_some(graph_style),
                with_content_format,
                diff_renderer.as_ref(),
                op_diff_changes_expr.clone(),
            )
            .await
        };
        Some(show)
    } else {
        None
    };

    ui.request_pager();
    let mut formatter = ui.stdout_formatter();
    let formatter = formatter.as_mut();
    let stream =
        op_walk::walk_ancestors(slice::from_ref(current_op)).take(args.limit.unwrap_or(usize::MAX));

    if !args.no_graph {
        let mut raw_output = formatter.raw()?;
        let mut graph = get_graphlog(graph_style, raw_output.as_mut());
        let stream = stream.map_ok(|op| {
            let ids = op.parent_ids();
            let edges = ids.iter().cloned().map(GraphEdge::direct).collect();
            (op, edges)
        });
        let mut stream_nodes: LocalBoxStream<'_, _> = if args.reversed {
            stream::iter(
                reverse_graph(stream.collect::<Vec<_>>().await.into_iter(), Operation::id)?
                    .into_iter()
                    .map(Ok),
            )
            .boxed()
        } else {
            stream.boxed_local()
        };
        while let Some(node) = stream_nodes.next().await {
            let (op, edges) = node?;
            let mut buffer = vec![];
            let within_graph = with_content_format.sub_width(graph.width(op.id(), &edges));
            within_graph
                .write(ui.new_formatter(&mut buffer).as_mut(), async |formatter| {
                    template.format(&op, formatter)
                })
                .await?;
            if let Some(show) = &maybe_show_op_diff {
                let mut formatter = ui.new_formatter(&mut buffer);
                show(ui, formatter.as_mut(), &op, &within_graph).await?;
            }
            let node_symbol = format_template(ui, &op, &op_node_template);
            graph.add_node(
                op.id(),
                &edges,
                &node_symbol,
                &String::from_utf8_lossy(&buffer),
            )?;
        }
    } else {
        let mut stream: LocalBoxStream<'_, _> = if args.reversed {
            stream::iter(stream.collect::<Vec<_>>().await.into_iter().rev()).boxed()
        } else {
            stream.boxed_local()
        };
        while let Some(op) = stream.try_next().await? {
            with_content_format
                .write(formatter, async |formatter| template.format(&op, formatter))
                .await?;
            if let Some(show) = &maybe_show_op_diff {
                show(ui, formatter, &op, &with_content_format).await?;
            }
        }
    }

    Ok(())
}
