// Copyright 2024 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::sync::Arc;

use futures::TryStreamExt as _;
use jj_cli::cli_util::CliRunner;
use jj_cli::commit_templater::CommitTemplateBuildFnTable;
use jj_cli::commit_templater::CommitTemplateLanguageExtension;
use jj_cli::template_parser;
use jj_cli::template_parser::TemplateParseError;
use jj_cli::templater::TemplatePropertyExt as _;
use jj_lib::backend::CommitId;
use jj_lib::commit::Commit;
use jj_lib::extensions_map::ExtensionsMap;
use jj_lib::object_id::ObjectId as _;
use jj_lib::repo::Repo;
use jj_lib::revset::FunctionCallNode;
use jj_lib::revset::LoweringContext;
use jj_lib::revset::PartialSymbolResolver;
use jj_lib::revset::RevsetDiagnostics;
use jj_lib::revset::RevsetExpression;
use jj_lib::revset::RevsetFilterExtension;
use jj_lib::revset::RevsetFilterPredicate;
use jj_lib::revset::RevsetParseError;
use jj_lib::revset::RevsetResolutionError;
use jj_lib::revset::SymbolResolverExtension;
use jj_lib::revset::UserRevsetExpression;
use once_cell::sync::OnceCell;
use pollster::FutureExt as _;

struct HexCounter;

fn num_digits_in_id(id: &CommitId) -> i64 {
    let mut count = 0;
    for ch in id.hex().chars() {
        if ch.is_ascii_digit() {
            count += 1;
        }
    }
    count
}

fn num_char_in_id(commit: Commit, ch_match: char) -> i64 {
    let mut count = 0;
    for ch in commit.id().hex().chars() {
        if ch == ch_match {
            count += 1;
        }
    }
    count
}

#[derive(Default)]
struct MostDigitsInId {
    count: OnceCell<i64>,
}

impl MostDigitsInId {
    fn count(&self, repo: &dyn Repo) -> i64 {
        *self.count.get_or_init(|| {
            RevsetExpression::all()
                .evaluate(repo)
                .unwrap()
                .stream()
                .try_collect::<Vec<_>>()
                .block_on()
                .unwrap()
                .iter()
                .map(num_digits_in_id)
                .max()
                .unwrap_or(0)
        })
    }
}

#[derive(Default)]
struct TheDigitestResolver {
    cache: MostDigitsInId,
}

impl PartialSymbolResolver for TheDigitestResolver {
    fn resolve_symbol(
        &self,
        repo: &dyn Repo,
        symbol: &str,
    ) -> Result<Option<CommitId>, RevsetResolutionError> {
        if symbol != "thedigitest" {
            return Ok(None);
        }

        Ok(RevsetExpression::all()
            .evaluate(repo)
            .map_err(|err| RevsetResolutionError::Other(err.into()))?
            .stream()
            .try_collect::<Vec<_>>()
            .block_on()
            .unwrap()
            .iter()
            .find(|id| num_digits_in_id(id) == self.cache.count(repo))
            .cloned())
    }
}

struct TheDigitest;

impl SymbolResolverExtension for TheDigitest {
    fn new_resolvers<'a>(&self, _repo: &'a dyn Repo) -> Vec<Box<dyn PartialSymbolResolver + 'a>> {
        vec![Box::<TheDigitestResolver>::default()]
    }
}

impl CommitTemplateLanguageExtension for HexCounter {
    fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo> {
        let mut table = CommitTemplateBuildFnTable::empty();
        table.commit_methods.insert(
            "has_most_digits",
            |language, _diagnostics, _build_context, property, call| {
                call.expect_no_arguments()?;
                let most_digits = language
                    .cache_extension::<MostDigitsInId>()
                    .unwrap()
                    .count(language.repo());
                let out_property =
                    property.map(move |commit| num_digits_in_id(commit.id()) == most_digits);
                Ok(out_property.into_dyn_wrapped())
            },
        );
        table.commit_methods.insert(
            "num_digits_in_id",
            |_language, _diagnostics, _build_context, property, call| {
                call.expect_no_arguments()?;
                let out_property = property.map(|commit| num_digits_in_id(commit.id()));
                Ok(out_property.into_dyn_wrapped())
            },
        );
        table.commit_methods.insert(
            "num_char_in_id",
            |_language, diagnostics, _build_context, property, call| {
                let [string_arg] = call.expect_exact_arguments()?;
                let char_arg = template_parser::catch_aliases(
                    diagnostics,
                    string_arg,
                    |_diagnostics, arg| {
                        let string = template_parser::expect_string_literal(arg)?;
                        let chars: Vec<_> = string.chars().collect();
                        match chars[..] {
                            [ch] => Ok(ch),
                            _ => Err(TemplateParseError::expression(
                                "Expected singular character argument",
                                arg.span,
                            )),
                        }
                    },
                )?;

                let out_property = property.map(move |commit| num_char_in_id(commit, char_arg));
                Ok(out_property.into_dyn_wrapped())
            },
        );

        table
    }

    fn build_cache_extensions(&self, extensions: &mut ExtensionsMap) {
        extensions.insert(MostDigitsInId::default());
    }
}

#[derive(Debug)]
struct EvenDigitsFilter;

impl RevsetFilterExtension for EvenDigitsFilter {
    fn matches_commit(&self, commit: &Commit) -> bool {
        num_digits_in_id(commit.id()) % 2 == 0
    }
}

fn even_digits(
    _diagnostics: &mut RevsetDiagnostics,
    function: &FunctionCallNode,
    _context: &LoweringContext,
) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
    function.expect_no_arguments()?;
    Ok(RevsetExpression::filter(RevsetFilterPredicate::Extension(
        Arc::new(EvenDigitsFilter),
    )))
}

fn main() -> std::process::ExitCode {
    CliRunner::init()
        .add_symbol_resolver_extension(Box::new(TheDigitest))
        .add_revset_function_extension("even_digits", even_digits)
        .add_commit_template_extension(Box::new(HexCounter))
        .run()
        .into()
}
