// 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.

mod advance;
mod create;
mod delete;
mod forget;
mod list;
mod r#move;
mod rename;
mod set;
mod track;
mod untrack;

use std::io;

use itertools::Itertools as _;
use jj_lib::backend::CommitId;
use jj_lib::iter_util::fallible_any;
use jj_lib::op_store::RefTarget;
use jj_lib::op_store::RemoteRef;
use jj_lib::ref_name::RefName;
use jj_lib::ref_name::RemoteName;
use jj_lib::ref_name::RemoteRefSymbol;
use jj_lib::repo::Repo;
use jj_lib::str_util::StringExpression;
use jj_lib::str_util::StringMatcher;
use jj_lib::view::View;

use self::advance::BookmarkAdvanceArgs;
use self::advance::cmd_bookmark_advance;
use self::create::BookmarkCreateArgs;
use self::create::cmd_bookmark_create;
use self::delete::BookmarkDeleteArgs;
use self::delete::cmd_bookmark_delete;
use self::forget::BookmarkForgetArgs;
use self::forget::cmd_bookmark_forget;
use self::list::BookmarkListArgs;
use self::list::cmd_bookmark_list;
use self::r#move::BookmarkMoveArgs;
use self::r#move::cmd_bookmark_move;
use self::rename::BookmarkRenameArgs;
use self::rename::cmd_bookmark_rename;
use self::set::BookmarkSetArgs;
use self::set::cmd_bookmark_set;
use self::track::BookmarkTrackArgs;
use self::track::cmd_bookmark_track;
use self::untrack::BookmarkUntrackArgs;
use self::untrack::cmd_bookmark_untrack;
use crate::cli_util::CommandHelper;
use crate::cli_util::RemoteBookmarkNamePattern;
use crate::command_error::CommandError;
use crate::ui::Ui;

// Unlike most other aliases, `b` is defined in the config and can be overridden
// by the user.

/// Manage bookmarks [default alias: b]
///
/// See [`jj help -k bookmarks`] for more information.
///
/// [`jj help -k bookmarks`]:
///     https://docs.jj-vcs.dev/latest/bookmarks
#[derive(clap::Subcommand, Clone, Debug)]
pub enum BookmarkCommand {
    #[command(visible_alias("a"))]
    Advance(BookmarkAdvanceArgs),
    #[command(visible_alias("c"))]
    Create(BookmarkCreateArgs),
    #[command(visible_alias("d"))]
    Delete(BookmarkDeleteArgs),
    #[command(visible_alias("f"))]
    Forget(BookmarkForgetArgs),
    #[command(visible_alias("l"))]
    List(BookmarkListArgs),
    #[command(visible_alias("m"))]
    Move(BookmarkMoveArgs),
    #[command(visible_alias("r"))]
    Rename(BookmarkRenameArgs),
    #[command(visible_alias("s"))]
    Set(BookmarkSetArgs),
    #[command(visible_alias("t"))]
    Track(BookmarkTrackArgs),
    Untrack(BookmarkUntrackArgs),
}

pub async fn cmd_bookmark(
    ui: &mut Ui,
    command: &CommandHelper,
    subcommand: &BookmarkCommand,
) -> Result<(), CommandError> {
    match subcommand {
        BookmarkCommand::Advance(args) => cmd_bookmark_advance(ui, command, args).await,
        BookmarkCommand::Create(args) => cmd_bookmark_create(ui, command, args).await,
        BookmarkCommand::Delete(args) => cmd_bookmark_delete(ui, command, args).await,
        BookmarkCommand::Forget(args) => cmd_bookmark_forget(ui, command, args).await,
        BookmarkCommand::List(args) => cmd_bookmark_list(ui, command, args).await,
        BookmarkCommand::Move(args) => cmd_bookmark_move(ui, command, args).await,
        BookmarkCommand::Rename(args) => cmd_bookmark_rename(ui, command, args).await,
        BookmarkCommand::Set(args) => cmd_bookmark_set(ui, command, args).await,
        BookmarkCommand::Track(args) => cmd_bookmark_track(ui, command, args).await,
        BookmarkCommand::Untrack(args) => cmd_bookmark_untrack(ui, command, args).await,
    }
}

fn find_trackable_remote_bookmarks<'a>(
    ui: &Ui,
    view: &'a View,
    name_patterns: &[RemoteBookmarkNamePattern],
) -> Result<Vec<(RemoteRefSymbol<'a>, &'a RemoteRef)>, CommandError> {
    let mut matching_bookmarks = vec![];
    let mut unmatched_symbols = vec![];
    for pattern in name_patterns {
        let bookmark_matcher = pattern.bookmark.to_matcher();
        let remote_matcher = pattern.remote.to_matcher();
        let mut matches =
            trackable_remote_bookmarks_matching(view, &bookmark_matcher, &remote_matcher)
                .peekable();
        if matches.peek().is_none() {
            unmatched_symbols.extend(pattern.as_exact());
        }
        matching_bookmarks.extend(matches);
    }
    matching_bookmarks.sort_unstable_by_key(|(sym, _)| *sym);
    matching_bookmarks.dedup_by(|(sym1, _), (sym2, _)| sym1 == sym2);
    if !unmatched_symbols.is_empty() {
        writeln!(
            ui.warning_default(),
            "No matching remote bookmarks for names: {}",
            unmatched_symbols.iter().join(", ")
        )?;
    }
    Ok(matching_bookmarks)
}

fn trackable_remote_bookmarks_matching<'a>(
    view: &'a View,
    bookmark_matcher: &StringMatcher,
    remote_matcher: &StringMatcher,
) -> impl Iterator<Item = (RemoteRefSymbol<'a>, &'a RemoteRef)> {
    let present_or_tracked_matches =
        view.remote_bookmarks_matching(bookmark_matcher, remote_matcher);
    let absent_matches =
        view.remote_views_matching(remote_matcher)
            .flat_map(move |(remote, remote_view)| {
                view.local_bookmarks_matching(bookmark_matcher)
                    .filter(|&(name, _)| !remote_view.bookmarks.contains_key(name))
                    .map(|(name, _)| (name.to_remote_symbol(remote), RemoteRef::absent_ref()))
            });
    itertools::chain(present_or_tracked_matches, absent_matches)
}

fn is_fast_forward(
    repo: &dyn Repo,
    old_target: &RefTarget,
    new_target_id: &CommitId,
) -> Result<bool, CommandError> {
    if old_target.is_present() {
        // Strictly speaking, "all" old targets should be ancestors, but we allow
        // conflict resolution by setting bookmark to "any" of the old target
        // descendants.
        let found = fallible_any(old_target.added_ids(), |old| {
            repo.index().is_ancestor(old, new_target_id)
        })?;
        Ok(found)
    } else {
        Ok(true)
    }
}

/// Warns about exact patterns that don't match local bookmarks.
fn warn_unmatched_local_bookmarks(
    ui: &Ui,
    view: &View,
    name_expr: &StringExpression,
) -> io::Result<()> {
    let mut names = name_expr
        .exact_strings()
        .map(RefName::new)
        .filter(|name| view.get_local_bookmark(name).is_absent())
        .peekable();
    if names.peek().is_none() {
        return Ok(());
    }
    writeln!(
        ui.warning_default(),
        "No matching bookmarks for names: {}",
        names.map(|name| name.as_symbol()).join(", ")
    )
}

/// Warns about exact patterns that don't match local or remote bookmarks.
fn warn_unmatched_local_or_remote_bookmarks(
    ui: &Ui,
    view: &View,
    name_expr: &StringExpression,
) -> io::Result<()> {
    let mut names = name_expr
        .exact_strings()
        .map(RefName::new)
        .filter(|&name| {
            view.get_local_bookmark(name).is_absent()
                && view
                    .remote_views()
                    .all(|(_, remote_view)| !remote_view.bookmarks.contains_key(name))
        })
        .peekable();
    if names.peek().is_none() {
        return Ok(());
    }
    writeln!(
        ui.warning_default(),
        "No matching bookmarks for names: {}",
        names.map(|name| name.as_symbol()).join(", ")
    )
}

/// Warns about exact patterns that don't match remotes.
fn warn_unmatched_remotes(ui: &Ui, view: &View, name_expr: &StringExpression) -> io::Result<()> {
    let mut names = name_expr
        .exact_strings()
        .map(RemoteName::new)
        .filter(|name| view.get_remote_view(name).is_none())
        .peekable();
    if names.peek().is_none() {
        return Ok(());
    }
    writeln!(
        ui.warning_default(),
        "No matching remotes for names: {}",
        names.map(|name| name.as_symbol()).join(", ")
    )
}
