"""API source and details"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.

# %% auto 0
__all__ = ['anki_defaults', 'data_path', 'profiles', 'close_all', 'Deck', 'add_card', 'add_fb_card', 'find_cards',
           'find_card_ids', 'find_notes', 'find_note_ids', 'update_note', 'update_fb_note', 'get_note', 'del_card',
           'sync', 'anki_tools']

# %% ../nbs/00_core.ipynb
import platform,gc
from fastcore.utils import *
from contextlib import closing
from pathlib import Path

from anki.collection import Collection
from anki import _backend
from anki.sync_pb2 import SyncAuth
from anki.errors import SyncError
from anki.models import ModelManager
from anki.notetypes_pb2 import NotetypeNameId
from anki.decks import DeckManager
from anki.decks_pb2 import DeckTreeNode, DeckNameId
from anki.cards import Card
from anki.notes import Note
from anki.collection_pb2 import OpChangesWithCount
from anki.sync_pb2 import SyncCollectionResponse

# %% ../nbs/00_core.ipynb
def data_path():
    "Return the default Anki data folder for this OS."
    syst = platform.system()
    path = Path(os.environ['APPDATA']) if syst=='Windows' else Path.home()
    return path/('.local/share/Anki2' if syst=='Linux'
        else 'Library/Application Support/Anki2' if syst=='Darwin'
        else 'Anki2')

# %% ../nbs/00_core.ipynb
def profiles():
    "List available Anki profile names."
    dp = data_path()
    if not dp.exists(): return []
    return [p.name for p in dp.iterdir() if (p/'collection.anki2').exists()]

# %% ../nbs/00_core.ipynb
# quiet down latency complaints
_backend.main_thread = lambda: None

# %% ../nbs/00_core.ipynb
anki_defaults = dict(profile='User 1', model='Basic', deck='Default')

# %% ../nbs/00_core.ipynb
@patch(cls_method=True)
def open(cls:Collection, profile=None):
    "Open a collection by profile name (creates if needed)."
    profile = profile or anki_defaults['profile']
    if not hasattr(cls, '_backend'): cls._backend = _backend.RustBackend()
    try: cls._backend.close_collection(downgrade_to_schema11=False)
    except: pass
    path = data_path() / profile
    path.mkdir(parents=True, exist_ok=True)
    col = cls(str(path / 'collection.anki2'), backend=cls._backend)
    col.profile_path = path
    return col

# %% ../nbs/00_core.ipynb
@patch(as_prop=True)
def _auth_path(self:Collection): return self.profile_path / 'sync_auth.bin'

@patch
def save_auth(self:Collection, auth):
    "Persist SyncAuth to profile folder."
    self._auth_path.write_bytes(auth.SerializeToString())

@patch
def load_auth(self:Collection):
    "Load SyncAuth from profile folder, or None if not found."
    p = self._auth_path
    if not p.exists(): return None
    auth = SyncAuth()
    auth.ParseFromString(p.read_bytes())
    return auth

# %% ../nbs/00_core.ipynb
@patch
def _repr_markdown_(self:SyncCollectionResponse):
    req = self.ChangesRequired.DESCRIPTOR.values_by_number[self.required].name
    return f"**Sync**: {req} (host: {self.host_number})"

# %% ../nbs/00_core.ipynb
@patch
def sync(self:Collection, user=None, passw=None, media=True, upload=False):
    auth = self.load_auth()
    if not auth or not auth.endpoint:
        if not (user and passw): raise ValueError("No saved auth; provide user and passw")
        auth = self.sync_login(username=user, password=passw, endpoint=None)
    status = self.sync_status(auth)
    if status.new_endpoint: auth = SyncAuth(hkey=auth.hkey, endpoint=status.new_endpoint)
    self.save_auth(auth)
    if status.required == status.Required.NO_CHANGES: return
    try: result = self.sync_collection(auth, sync_media=media)
    except SyncError:
        if not (user and passw): raise
        auth = self.sync_login(username=user, password=passw, endpoint=auth.endpoint)
        result = self.sync_collection(auth, sync_media=media)
    if result.new_endpoint: auth = SyncAuth(hkey=auth.hkey, endpoint=result.new_endpoint)
    self.save_auth(auth)
    needs_full = (result.ChangesRequired.FULL_SYNC, result.ChangesRequired.FULL_DOWNLOAD, result.ChangesRequired.FULL_UPLOAD)
    if result.required in needs_full:
        do_upload = upload if result.required == result.ChangesRequired.FULL_SYNC else result.required == result.ChangesRequired.FULL_UPLOAD
        self.close_for_full_sync()
        self.full_upload_or_download(auth=auth, server_usn=result.server_media_usn, upload=do_upload)
        self.reopen(after_full_sync=True)
    return result

# %% ../nbs/00_core.ipynb
def close_all():
    "Close all open Anki backends (nuclear option for crash recovery)."
    for obj in gc.get_objects():
        if isinstance(obj, RustBackend):
            try: obj.close_collection(downgrade_to_schema11=False)
            except: pass

# %% ../nbs/00_core.ipynb
@patch
def _repr_markdown_(self:ModelManager): return '\n'.join('- '+m.name for m in self.all_names_and_ids())

@patch
def _repr_markdown_(self:NotetypeNameId): return f"**{self.name}** (id: {self.id})"

# %% ../nbs/00_core.ipynb
@patch
def _repr_markdown_(self:DeckManager): return '\n'.join('- '+d.name for d in self.all_names_and_ids())

@patch
def _repr_markdown_(self:DeckNameId): return f"**{self.name}** (id: {self.id})"

@patch
def _repr_markdown_(self:DeckTreeNode):
    return f"**{self.name}**: {self.new_count} new, {self.learn_count} learn, {self.review_count} review"

# %% ../nbs/00_core.ipynb
ModelManager.__getitem__ = ModelManager.by_name

# %% ../nbs/00_core.ipynb
@patch
def _repr_markdown_(self:Note):
    fields = ' | '.join(f"**{k}**: {self[k]}" for k in self._fmap)
    return f"{fields} | 🏷️ {', '.join(self.tags)}" if self.tags else fields

@patch
def __repr__(self:Note):
    fields = ', '.join(f"{k}={self[k]!r}" for k in self._fmap)
    return f"Note({self.id}, {fields}, tags={self.tags})"

# %% ../nbs/00_core.ipynb
@patch
def _repr_markdown_(self:OpChangesWithCount): return f"✓ {self.count} change(s)"

# %% ../nbs/00_core.ipynb
class Deck:
    def __init__(self, col, name):
        self.col, self.name = col, name
        self.id = col.decks.id_for_name(name)
    
    def add(self, model=None, tags=None, **fields): return self.col.add(model=model, deck=self.name, tags=tags, **fields)
    
    @property
    def due(self): return self.col.sched.deck_due_tree(self.id)
    
    @property
    def cards(self): return self.col.find_cards(f"deck:{self.name}")
    
    def _repr_markdown_(self):
        n, l, r = self.due
        return f"**{self.name}**: {n} new, {l} learn, {r} review"

# %% ../nbs/00_core.ipynb
@patch
def __getitem__(self:DeckManager, name): return Deck(self.col, name)

# %% ../nbs/00_core.ipynb
@patch
def add(self:Collection, model=None, deck=None, tags=None, **fields):
    "Add a note with the given fields."
    model, deck = model or anki_defaults['model'], deck or anki_defaults['deck']
    note = self.new_note(self.models[model])
    for k,v in fields.items(): note[k] = v
    if tags: note.tags = tags if isinstance(tags, list) else [tags]
    res = self.add_note(note, self.decks.id_for_name(deck))
    if res.count == 0: raise ValueError("Failed to add note")
    return note

# %% ../nbs/00_core.ipynb
@patch
def add_deck(self:Collection, name):
    "Create a deck (use :: for nesting)."
    return self.decks.add_normal_deck_with_name(name)

# %% ../nbs/00_core.ipynb
@patch
def __repr__(self:ModelManager): return f"ModelManager({[m.name for m in self.all_names_and_ids()]})"

@patch
def __repr__(self:NotetypeNameId): return f"NotetypeNameId({self.id!r}, {self.name!r})"

@patch
def __repr__(self:DeckManager): return f"DeckManager({[d.name for d in self.all_names_and_ids()]})"

@patch
def __repr__(self:DeckNameId): return f"DeckNameId({self.id!r}, {self.name!r})"

@patch
def __repr__(self:DeckTreeNode):
    return f"DeckTreeNode({self.deck_id!r}, {self.name!r}, new={self.new_count}, learn={self.learn_count}, review={self.review_count})"

@patch
def __repr__(self:OpChangesWithCount): return f"OpChangesWithCount({self.count})"

@patch
def __repr__(self:Card): return f"Card({self.id}, nid={self.nid}, due={self.due}, ivl={self.ivl}, queue={self.queue})"

@patch
def __repr__(self:Deck): return f"Deck({self.name!r}, id={self.id})"

# %% ../nbs/00_core.ipynb
@patch
def __enter__(self:Collection): return self

@patch
def __exit__(self:Collection, *args): self.close()

# %% ../nbs/00_core.ipynb
def add_card(profile=None, model=None, deck=None, tags=None, **fields):
    "Add a card."
    profile, model, deck = profile or anki_defaults['profile'], model or anki_defaults['model'], deck or anki_defaults['deck']
    with Collection.open(profile) as col: return col.add(model=model, deck=deck, tags=tags or None, **fields)

# %% ../nbs/00_core.ipynb
def add_fb_card(front:str, back:str, profile:str=None, model:str=None, deck:str=None, tags:str=None):
    "Add a card with a `Front` and `Back` and return the id."
    return add_card(profile=profile, model=model, deck=deck, tags=tags, Front=front, Back=back).id

# %% ../nbs/00_core.ipynb
def find_cards(query:str, profile:str=None):
    "Find cards matching query string. Returns list of Note objects."
    profile = profile or anki_defaults['profile']
    with Collection.open(profile) as col: return [col.get_card(cid) for cid in col.find_cards(query)]

# %% ../nbs/00_core.ipynb
@patch
def _repr_markdown_(self:Card):
    return f"Card {self.id} (nid: {self.nid}, due: {self.due}, ivl: {self.ivl}d, queue: {self.queue})"

# %% ../nbs/00_core.ipynb
def find_card_ids(query:str, profile:str=None):
    "Find card ids matching query string."
    profile = profile or anki_defaults['profile']
    with Collection.open(profile) as col: return col.find_cards(query)

# %% ../nbs/00_core.ipynb
def find_notes(query:str, profile:str=None):
    "Find notes matching query string. Returns list of Note objects."
    profile = profile or anki_defaults['profile']
    with Collection.open(profile) as col: return [col.get_note(nid) for nid in col.find_notes(query)]

# %% ../nbs/00_core.ipynb
def find_note_ids(query:str, profile:str=None):
    "Find note ids matching query string."
    profile = profile or anki_defaults['profile']
    with Collection.open(profile) as col: return col.find_notes(query)

# %% ../nbs/00_core.ipynb
def update_note(note:int, profile:str=None, tags:str=None, add_tags:str=None, **fields):
    "Update an existing note's fields and/or tags. Pass a Note object or note ID."
    profile = profile or anki_defaults['profile']
    with Collection.open(profile) as col:
        if not isinstance(note, Note): note = col.get_note(note)
        for k,v in fields.items(): note[k] = v
        if tags: note.tags = listify(tags)
        note.tags = list(set(listify(note.tags) + listify(add_tags)))
        col.update_note(note)
        return note

# %% ../nbs/00_core.ipynb
def update_fb_note(note_id:int, front:str='', back:str='', profile:str=None, tags:str=None, add_tags:str=None):
    "Update an existing note's front/back fields and/or tags. Pass a Note object or note ID."
    kw = {}
    if front: kw['Front'] = front
    if back: kw['Back'] = back
    return update_note(note_id, profile=profile, tags=tags, add_tags=add_tags, **kw)

# %% ../nbs/00_core.ipynb
def get_note(note_id:int, profile:str=None):
    "Retrieve a note by ID."
    profile = profile or anki_defaults['profile']
    with Collection.open(profile) as col: return col.get_note(note_id)

# %% ../nbs/00_core.ipynb
def del_card(notes:int, profile:str=None):
    "Delete card(s) by Note(s) or note id(s)."
    profile = profile or anki_defaults['profile']
    nids = [n if isinstance(n, int) else n.id for n in listify(notes)]
    with Collection.open(profile) as col: return col.remove_notes(nids)

# %% ../nbs/00_core.ipynb
def sync(profile:str=None, user:str=None, passw:str=None, media:bool=True):
    "Sync collection, handling open/close automatically."
    profile = profile or anki_defaults['profile']
    with Collection.open(profile) as col: return col.sync(user=user, passw=passw, media=media)

# %% ../nbs/00_core.ipynb
def anki_tools(): print('&`[add_fb_card, find_notes, find_note_ids, find_cards, find_card_ids, get_note, del_card, update_fb_note, sync]`')
