// This file is part of radicle-git
// <https://github.com/radicle-dev/radicle-git>
//
// Copyright (C) 2022 The Radicle Team <dev@radicle.xyz>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 or
// later as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use std::marker::PhantomData;

use git_ext::ref_format::{
    self,
    refname,
    refspec::{self, PatternString, QualifiedPattern},
    Qualified,
    RefStr,
    RefString,
};
use thiserror::Error;

use crate::{Branch, Local, Namespace, Remote, Tag};

#[derive(Debug, Error)]
pub enum Error {
    #[error(transparent)]
    RefFormat(#[from] ref_format::Error),
}

/// A collection of globs for a git reference type.
#[derive(Clone, Debug)]
pub struct Glob<T> {
    globs: Vec<QualifiedPattern<'static>>,
    glob_type: PhantomData<T>, // To support different methods for different T.
}

impl<T> Default for Glob<T> {
    fn default() -> Self {
        Self {
            globs: Default::default(),
            glob_type: PhantomData,
        }
    }
}

impl<T> Glob<T> {
    /// Return the [`QualifiedPattern`] globs of this `Glob`.
    pub fn globs(&self) -> impl Iterator<Item = &QualifiedPattern<'static>> {
        self.globs.iter()
    }

    /// Combine two `Glob`s together by combining their glob lists together.
    ///
    /// Note that the `Glob`s must result in the same type,
    /// e.g. `Glob<Tag>` can only combine with `Glob<Tag>`,
    /// `Glob<Local>` can combine with `Glob<Remote>`, etc.
    pub fn and(mut self, other: impl Into<Self>) -> Self {
        self.globs.extend(other.into().globs);
        self
    }
}

impl Glob<Namespace> {
    /// Creates the `Glob` that mathces all `refs/namespaces`.
    pub fn all_namespaces() -> Self {
        Self::namespaces(refspec::pattern!("*"))
    }

    /// Creates a `Glob` for `refs/namespaces`, starting with `glob`.
    pub fn namespaces(glob: PatternString) -> Self {
        let globs = vec![Self::qualify(glob)];
        Self {
            globs,
            glob_type: PhantomData,
        }
    }

    /// Adds a `refs/namespaces` pattern to this `Glob`.
    pub fn insert(mut self, glob: PatternString) -> Self {
        self.globs.push(Self::qualify(glob));
        self
    }

    fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
        qualify(&refname!("refs/namespaces"), glob).expect("BUG: pattern is qualified")
    }
}

impl FromIterator<PatternString> for Glob<Namespace> {
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
        let globs = iter
            .into_iter()
            .map(|pat| {
                qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
            })
            .collect();

        Self {
            globs,
            glob_type: PhantomData,
        }
    }
}

impl Extend<PatternString> for Glob<Namespace> {
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
        self.globs.extend(iter.into_iter().map(|pat| {
            qualify(&refname!("refs/namespaces"), pat).expect("BUG: pattern is qualified")
        }))
    }
}

impl Glob<Tag> {
    /// Creates a `Glob` that matches all `refs/tags`.
    pub fn all_tags() -> Self {
        Self::tags(refspec::pattern!("*"))
    }

    /// Creates a `Glob` for `refs/tags`, starting with `glob`.
    pub fn tags(glob: PatternString) -> Self {
        let globs = vec![Self::qualify(glob)];
        Self {
            globs,
            glob_type: PhantomData,
        }
    }

    /// Adds a `refs/tags` pattern to this `Glob`.
    pub fn insert(mut self, glob: PatternString) -> Self {
        self.globs.push(Self::qualify(glob));
        self
    }

    fn qualify(glob: PatternString) -> QualifiedPattern<'static> {
        qualify(&refname!("refs/tags"), glob).expect("BUG: pattern is qualified")
    }
}

impl FromIterator<PatternString> for Glob<Tag> {
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
        let globs = iter
            .into_iter()
            .map(|pat| qualify(&refname!("refs/tags"), pat).expect("BUG: pattern is qualified"))
            .collect();

        Self {
            globs,
            glob_type: PhantomData,
        }
    }
}

impl Extend<PatternString> for Glob<Tag> {
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
        self.globs.extend(
            iter.into_iter()
                .map(|pat| qualify(&refname!("refs/tag"), pat).expect("BUG: pattern is qualified")),
        )
    }
}

impl Glob<Local> {
    /// Creates the `Glob` that mathces all `refs/heads`.
    pub fn all_heads() -> Self {
        Self::heads(refspec::pattern!("*"))
    }

    /// Creates a `Glob` for `refs/heads`, starting with `glob`.
    pub fn heads(glob: PatternString) -> Self {
        let globs = vec![Self::qualify_heads(glob)];
        Self {
            globs,
            glob_type: PhantomData,
        }
    }

    /// Adds a `refs/heads` pattern to this `Glob`.
    pub fn insert(mut self, glob: PatternString) -> Self {
        self.globs.push(Self::qualify_heads(glob));
        self
    }

    /// When chaining `Glob<Local>` with `Glob<Remote>`, use
    /// `branches` to convert this `Glob<Local>` into a
    /// `Glob<Branch>`.
    ///
    /// # Example
    /// ```no_run
    /// Glob::heads(pattern!("features/*"))
    ///     .insert(pattern!("qa/*"))
    ///     .branches()
    ///     .and(Glob::remotes(pattern!("origin/features/*")))
    /// ```
    pub fn branches(self) -> Glob<Branch> {
        self.into()
    }

    fn qualify_heads(glob: PatternString) -> QualifiedPattern<'static> {
        qualify(&refname!("refs/heads"), glob).expect("BUG: pattern is qualified")
    }
}

impl FromIterator<PatternString> for Glob<Local> {
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
        let globs = iter
            .into_iter()
            .map(|pat| qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified"))
            .collect();

        Self {
            globs,
            glob_type: PhantomData,
        }
    }
}

impl Extend<PatternString> for Glob<Local> {
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
        self.globs.extend(
            iter.into_iter().map(|pat| {
                qualify(&refname!("refs/heads"), pat).expect("BUG: pattern is qualified")
            }),
        )
    }
}

impl From<Glob<Local>> for Glob<Branch> {
    fn from(Glob { globs, .. }: Glob<Local>) -> Self {
        Self {
            globs,
            glob_type: PhantomData,
        }
    }
}

impl Glob<Remote> {
    /// Creates the `Glob` that mathces all `refs/remotes`.
    pub fn all_remotes() -> Self {
        Self::remotes(refspec::pattern!("*"))
    }

    /// Creates a `Glob` for `refs/remotes`, starting with `glob`.
    pub fn remotes(glob: PatternString) -> Self {
        let globs = vec![Self::qualify_remotes(glob)];
        Self {
            globs,
            glob_type: PhantomData,
        }
    }

    /// Adds a `refs/remotes` pattern to this `Glob`.
    pub fn insert(mut self, glob: PatternString) -> Self {
        self.globs.push(Self::qualify_remotes(glob));
        self
    }

    /// When chaining `Glob<Remote>` with `Glob<Local>`, use
    /// `branches` to convert this `Glob<Remote>` into a
    /// `Glob<Branch>`.
    ///
    /// # Example
    /// ```no_run
    /// Glob::remotes(pattern!("origin/features/*"))
    ///     .insert(pattern!("origin/qa/*"))
    ///     .branches()
    ///     .and(Glob::heads(pattern!("features/*")))
    /// ```
    pub fn branches(self) -> Glob<Branch> {
        self.into()
    }

    fn qualify_remotes(glob: PatternString) -> QualifiedPattern<'static> {
        qualify(&refname!("refs/remotes"), glob).expect("BUG: pattern is qualified")
    }
}

impl FromIterator<PatternString> for Glob<Remote> {
    fn from_iter<T: IntoIterator<Item = PatternString>>(iter: T) -> Self {
        let globs = iter
            .into_iter()
            .map(|pat| qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified"))
            .collect();

        Self {
            globs,
            glob_type: PhantomData,
        }
    }
}

impl Extend<PatternString> for Glob<Remote> {
    fn extend<T: IntoIterator<Item = PatternString>>(&mut self, iter: T) {
        self.globs.extend(
            iter.into_iter().map(|pat| {
                qualify(&refname!("refs/remotes"), pat).expect("BUG: pattern is qualified")
            }),
        )
    }
}

impl From<Glob<Remote>> for Glob<Branch> {
    fn from(Glob { globs, .. }: Glob<Remote>) -> Self {
        Self {
            globs,
            glob_type: PhantomData,
        }
    }
}

impl Glob<Qualified<'_>> {
    pub fn all_category<R: AsRef<RefStr>>(category: R) -> Self {
        Self {
            globs: vec![Self::qualify_category(category, refspec::pattern!("*"))],
            glob_type: PhantomData,
        }
    }

    /// Creates a `Glob` for `refs/<category>`, starting with `glob`.
    pub fn categories<R>(category: R, glob: PatternString) -> Self
    where
        R: AsRef<RefStr>,
    {
        let globs = vec![Self::qualify_category(category, glob)];
        Self {
            globs,
            glob_type: PhantomData,
        }
    }

    /// Adds a `refs/<category>` pattern to this `Glob`.
    pub fn insert<R>(mut self, category: R, glob: PatternString) -> Self
    where
        R: AsRef<RefStr>,
    {
        self.globs.push(Self::qualify_category(category, glob));
        self
    }

    fn qualify_category<R>(category: R, glob: PatternString) -> QualifiedPattern<'static>
    where
        R: AsRef<RefStr>,
    {
        let prefix = refname!("refs").and(category);
        qualify(&prefix, glob).expect("BUG: pattern is qualified")
    }
}

fn qualify(prefix: &RefString, glob: PatternString) -> Option<QualifiedPattern<'static>> {
    prefix.to_pattern(glob).qualified().map(|q| q.into_owned())
}
