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

#![allow(missing_docs)]

use std::collections::{BTreeMap, HashMap, HashSet};

use itertools::Itertools;

use crate::backend::CommitId;
use crate::op_store::{BranchTarget, RefTarget, RefTargetOptionExt as _, RemoteRef, WorkspaceId};
use crate::refs::TrackingRefPair;
use crate::str_util::StringPattern;
use crate::{op_store, refs};

#[derive(PartialEq, Eq, Debug, Clone)]
pub struct View {
    data: op_store::View,
}

impl View {
    pub fn new(op_store_view: op_store::View) -> Self {
        View {
            data: op_store_view,
        }
    }

    pub fn wc_commit_ids(&self) -> &HashMap<WorkspaceId, CommitId> {
        &self.data.wc_commit_ids
    }

    pub fn get_wc_commit_id(&self, workspace_id: &WorkspaceId) -> Option<&CommitId> {
        self.data.wc_commit_ids.get(workspace_id)
    }

    pub fn workspaces_for_wc_commit_id(&self, commit_id: &CommitId) -> Vec<WorkspaceId> {
        let mut workspaces_ids = vec![];
        for (workspace_id, wc_commit_id) in &self.data.wc_commit_ids {
            if wc_commit_id == commit_id {
                workspaces_ids.push(workspace_id.clone());
            }
        }
        workspaces_ids
    }

    pub fn is_wc_commit_id(&self, commit_id: &CommitId) -> bool {
        self.data.wc_commit_ids.values().contains(commit_id)
    }

    pub fn heads(&self) -> &HashSet<CommitId> {
        &self.data.head_ids
    }

    pub fn public_heads(&self) -> &HashSet<CommitId> {
        &self.data.public_head_ids
    }

    /// Iterates pair of local and remote branches by branch name.
    pub fn branches(&self) -> impl Iterator<Item = (&str, BranchTarget<'_>)> {
        op_store::merge_join_branch_views(&self.data.local_branches, &self.data.remote_views)
    }

    pub fn tags(&self) -> &BTreeMap<String, RefTarget> {
        &self.data.tags
    }

    pub fn git_refs(&self) -> &BTreeMap<String, RefTarget> {
        &self.data.git_refs
    }

    pub fn git_head(&self) -> &RefTarget {
        &self.data.git_head
    }

    pub fn set_wc_commit(&mut self, workspace_id: WorkspaceId, commit_id: CommitId) {
        self.data.wc_commit_ids.insert(workspace_id, commit_id);
    }

    pub fn remove_wc_commit(&mut self, workspace_id: &WorkspaceId) {
        self.data.wc_commit_ids.remove(workspace_id);
    }

    pub fn add_head(&mut self, head_id: &CommitId) {
        self.data.head_ids.insert(head_id.clone());
    }

    pub fn remove_head(&mut self, head_id: &CommitId) {
        self.data.head_ids.remove(head_id);
    }

    pub fn add_public_head(&mut self, head_id: &CommitId) {
        self.data.public_head_ids.insert(head_id.clone());
    }

    pub fn remove_public_head(&mut self, head_id: &CommitId) {
        self.data.public_head_ids.remove(head_id);
    }

    /// Returns true if any local or remote branch of the given `name` exists.
    #[must_use]
    pub fn has_branch(&self, name: &str) -> bool {
        self.data.local_branches.contains_key(name)
            || self
                .data
                .remote_views
                .values()
                .any(|remote_view| remote_view.branches.contains_key(name))
    }

    // TODO: maybe rename to forget_branch() because this seems unusual operation?
    pub fn remove_branch(&mut self, name: &str) {
        self.data.local_branches.remove(name);
        for remote_view in self.data.remote_views.values_mut() {
            remote_view.branches.remove(name);
        }
    }

    /// Iterates local branch `(name, target)`s in lexicographical order.
    pub fn local_branches(&self) -> impl Iterator<Item = (&str, &RefTarget)> {
        self.data
            .local_branches
            .iter()
            .map(|(name, target)| (name.as_ref(), target))
    }

    /// Iterates local branch `(name, target)`s matching the given pattern.
    /// Entries are sorted by `name`.
    pub fn local_branches_matching<'a: 'b, 'b>(
        &'a self,
        pattern: &'b StringPattern,
    ) -> impl Iterator<Item = (&'a str, &'a RefTarget)> + 'b {
        pattern
            .filter_btree_map(&self.data.local_branches)
            .map(|(name, target)| (name.as_ref(), target))
    }

    pub fn get_local_branch(&self, name: &str) -> &RefTarget {
        self.data.local_branches.get(name).flatten()
    }

    /// Sets local branch to point to the given target. If the target is absent,
    /// and if no associated remote branches exist, the branch will be removed.
    pub fn set_local_branch_target(&mut self, name: &str, target: RefTarget) {
        if target.is_present() {
            self.data.local_branches.insert(name.to_owned(), target);
        } else {
            self.data.local_branches.remove(name);
        }
    }

    /// Iterates remote branch `((name, remote_name), remote_ref)`s in
    /// lexicographical order.
    pub fn all_remote_branches(&self) -> impl Iterator<Item = ((&str, &str), &RemoteRef)> {
        op_store::flatten_remote_branches(&self.data.remote_views)
    }

    /// Iterates branch `(name, remote_ref)`s of the specified remote in
    /// lexicographical order.
    pub fn remote_branches(&self, remote_name: &str) -> impl Iterator<Item = (&str, &RemoteRef)> {
        let maybe_remote_view = self.data.remote_views.get(remote_name);
        maybe_remote_view
            .map(|remote_view| {
                remote_view
                    .branches
                    .iter()
                    .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
            })
            .into_iter()
            .flatten()
    }

    /// Iterates remote branch `((name, remote_name), remote_ref)`s matching the
    /// given patterns. Entries are sorted by `(name, remote_name)`.
    pub fn remote_branches_matching<'a: 'b, 'b>(
        &'a self,
        branch_pattern: &'b StringPattern,
        remote_pattern: &'b StringPattern,
    ) -> impl Iterator<Item = ((&'a str, &'a str), &'a RemoteRef)> + 'b {
        // Use kmerge instead of flat_map for consistency with all_remote_branches().
        remote_pattern
            .filter_btree_map(&self.data.remote_views)
            .map(|(remote_name, remote_view)| {
                branch_pattern.filter_btree_map(&remote_view.branches).map(
                    |(branch_name, remote_ref)| {
                        let full_name = (branch_name.as_ref(), remote_name.as_ref());
                        (full_name, remote_ref)
                    },
                )
            })
            .kmerge_by(|(full_name1, _), (full_name2, _)| full_name1 < full_name2)
    }

    pub fn get_remote_branch(&self, name: &str, remote_name: &str) -> &RemoteRef {
        if let Some(remote_view) = self.data.remote_views.get(remote_name) {
            remote_view.branches.get(name).flatten()
        } else {
            RemoteRef::absent_ref()
        }
    }

    /// Sets remote-tracking branch to the given target and state. If the target
    /// is absent, the branch will be removed.
    pub fn set_remote_branch(&mut self, name: &str, remote_name: &str, remote_ref: RemoteRef) {
        if remote_ref.is_present() {
            let remote_view = self
                .data
                .remote_views
                .entry(remote_name.to_owned())
                .or_default();
            remote_view.branches.insert(name.to_owned(), remote_ref);
        } else if let Some(remote_view) = self.data.remote_views.get_mut(remote_name) {
            remote_view.branches.remove(name);
        }
    }

    /// Iterates local/remote branch `(name, remote_ref)`s of the specified
    /// remote in lexicographical order.
    pub fn local_remote_branches<'a>(
        &'a self,
        remote_name: &str,
    ) -> impl Iterator<Item = (&'a str, TrackingRefPair<'a>)> + 'a {
        refs::iter_named_local_remote_refs(self.local_branches(), self.remote_branches(remote_name))
            .map(|(name, (local_target, remote_ref))| {
                let targets = TrackingRefPair {
                    local_target,
                    remote_ref,
                };
                (name, targets)
            })
    }

    /// Iterates local/remote branch `(name, remote_ref)`s of the specified
    /// remote, matching the given branch name pattern. Entries are sorted by
    /// `name`.
    pub fn local_remote_branches_matching<'a: 'b, 'b>(
        &'a self,
        branch_pattern: &'b StringPattern,
        remote_name: &str,
    ) -> impl Iterator<Item = (&'a str, TrackingRefPair<'a>)> + 'b {
        // Change remote_name to StringPattern if needed, but merge-join adapter won't
        // be usable.
        let maybe_remote_view = self.data.remote_views.get(remote_name);
        refs::iter_named_local_remote_refs(
            branch_pattern.filter_btree_map(&self.data.local_branches),
            maybe_remote_view
                .map(|remote_view| branch_pattern.filter_btree_map(&remote_view.branches))
                .into_iter()
                .flatten(),
        )
        .map(|(name, (local_target, remote_ref))| {
            let targets = TrackingRefPair {
                local_target,
                remote_ref,
            };
            (name.as_ref(), targets)
        })
    }

    pub fn remove_remote(&mut self, remote_name: &str) {
        self.data.remote_views.remove(remote_name);
    }

    pub fn rename_remote(&mut self, old: &str, new: &str) {
        if let Some(remote_view) = self.data.remote_views.remove(old) {
            self.data.remote_views.insert(new.to_owned(), remote_view);
        }
    }

    pub fn get_tag(&self, name: &str) -> &RefTarget {
        self.data.tags.get(name).flatten()
    }

    /// Sets tag to point to the given target. If the target is absent, the tag
    /// will be removed.
    pub fn set_tag_target(&mut self, name: &str, target: RefTarget) {
        if target.is_present() {
            self.data.tags.insert(name.to_owned(), target);
        } else {
            self.data.tags.remove(name);
        }
    }

    pub fn get_git_ref(&self, name: &str) -> &RefTarget {
        self.data.git_refs.get(name).flatten()
    }

    /// Sets the last imported Git ref to point to the given target. If the
    /// target is absent, the reference will be removed.
    pub fn set_git_ref_target(&mut self, name: &str, target: RefTarget) {
        if target.is_present() {
            self.data.git_refs.insert(name.to_owned(), target);
        } else {
            self.data.git_refs.remove(name);
        }
    }

    /// Sets `HEAD@git` to point to the given target. If the target is absent,
    /// the reference will be cleared.
    pub fn set_git_head_target(&mut self, target: RefTarget) {
        self.data.git_head = target;
    }

    pub fn set_view(&mut self, data: op_store::View) {
        self.data = data;
    }

    pub fn store_view(&self) -> &op_store::View {
        &self.data
    }

    pub fn store_view_mut(&mut self) -> &mut op_store::View {
        &mut self.data
    }
}
