"""Atiyah–Patodi–Singer eta invariants of lens spaces -- numberdb.org/T622.

Run with SageMath (or passagemath with numberdb installed):
    sage -pip install numberdb
    sage -python generate.py                 # verify every stored entry
    sage -python checks.py                   # independent local checks

The exact values are -4 times Toffoli's shifted Dedekind sum s_{0,u/p}(q,p),
Theorem 3.3.27 of his 2019 thesis. The sawtooth is zero at integers.
The operator acts on even-degree complex forms, with the sign fixed by the
negative untwisted cotangent sum. No kernel term or reduction modulo Z is used.

Default selection: 2 <= p <= 20; q is the smaller of q and its inverse
modulo p; 0 <= u <= floor(p/2). See argument-selection.md for the rationale.
Changing q to its inverse also changes u to u*q^(-1) modulo p. Negating q
reverses orientation, so it is not used to remove presentations.

Authorized contributors may send entries to this existing table with
    NUMBERDB_API_KEY=... sage -python generate.py --fill
This writes entries and source attachments; it does not offer, review or
publish the website draft. Authentication can instead arrive on stdin with
NUMBERDB_KEY_FROM_STDIN=1. NUMBERDB_PUBLISH=1 is the campaign wrapper's
entries-only switch. The generator never creates a table.
"""

import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from math import gcd
import os
from pathlib import Path
import sys

MAX_P = 20


def sawtooth_residue(n, p):
    """The exact sawtooth ((n/p)), including zero at integral arguments."""
    residue = n % p
    return QQ(0) if residue == 0 else QQ(residue) / QQ(p) - QQ(1) / QQ(2)


def eta_exact(p, q, u):
    """Unreduced eta for the unit round quotient and the complex line chi_u."""
    p, q, u = int(p), int(q), int(u)
    if p < 2 or not 1 <= q < p or gcd(p, q) != 1 or not 0 <= u < p:
        raise ValueError('Require p >= 2, 1 <= q < p, gcd(p,q)=1, 0 <= u < p')
    return -4 * sum((sawtooth_residue(j, p) * sawtooth_residue(q*j+u, p)
                     for j in range(p)), QQ(0))


class LensEta(numberdb.Generator):
    table = 'T622'
    parameters = ('p', 'q', 'u')
    type = 'Q'
    rigour = 'exact'
    digits = 100
    files = ('generate.py', 'checks.py', 'argument-selection.md',
             'numerical-evidence.json', 'skill-manifest.json', 'sources.md',
             'check.py')

    def enumerate(self, max_p=MAX_P):
        for p in range(2, int(max_p)+1):
            for q in range(1, p):
                if gcd(p, q) == 1 and q <= pow(q, -1, p):
                    for u in range(p//2+1):
                        yield {'p': str(p), 'q': str(q), 'u': str(u)}

    def value(self, params, digits):
        return eta_exact(*(params[key] for key in self.parameters))


def client_from_environment():
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        key = sys.stdin.read().strip()
        if '=' in key and key.split('=', 1)[0].isidentifier():
            key = key.split('=', 1)[1].strip().strip('\"\'')
        return numberdb.Client(api_key=key)
    return numberdb.Client()


if __name__ == '__main__':
    client = client_from_environment()
    generator = LensEta()
    if '--fill' in sys.argv or os.environ.get('NUMBERDB_PUBLISH') == '1':
        print(generator.publish(client=client, assisted_by=os.environ.get(
            'NUMBERDB_ASSISTED_BY', ''), message='Fill exact lens-space eta invariants after independent arithmetic and ball checks'))
    else:
        report = generator.verify(sample=None, client=client)
        print(report)
        sys.exit(0 if report.ok else 1)
