#!/usr/local/bin/python3.12
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>


import json
import os
import sys
from textwrap import TextWrapper

from calibre.db.cli.utils import str_width
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.date import isoformat
from polyglot.builtins import as_bytes, iteritems

readonly = True
version = 0  # change this if you change signature of implementation()
FIELDS = {
    'title', 'authors', 'author_sort', 'publisher', 'rating', 'timestamp', 'size',
    'tags', 'comments', 'series', 'series_index', 'formats', 'isbn', 'uuid',
    'pubdate', 'cover', 'last_modified', 'identifiers', 'languages'
}


def formats(db, book_id):
    for fmt in db.formats(book_id, verify_formats=False):
        path = db.format_abspath(book_id, fmt)
        if path:
            yield path.replace(os.sep, '/')


def cover(db, book_id):
    return db.format_abspath(book_id, '__COVER_INTERNAL__')


def implementation(
    db, notify_changes, fields, sort_by, ascending, search_text, limit
):
    is_remote = notify_changes is not None
    with db.safe_read_lock:
        fm = db.field_metadata
        afields = set(FIELDS) | {'id'}
        for k in fm.custom_field_keys():
            afields.add('*' + k[1:])
        if 'all' in fields:
            fields = sorted(afields)
        sort_by = sort_by or 'id'
        if sort_by not in afields:
            return f'Unknown sort field: {sort_by}'
        if not set(fields).issubset(afields):
            return 'Unknown fields: {}'.format(', '.join(set(fields) - afields))
        if search_text:
            book_ids = db.multisort([(sort_by, ascending)],
                                    ids_to_sort=db.search(search_text))
        else:
            book_ids = db.multisort([(sort_by, ascending)])
        if limit > -1:
            book_ids = book_ids[:limit]
        data = {}
        metadata = {}
        for field in fields:
            if field in 'id':
                continue
            if field == 'isbn':
                x = db.all_field_for('identifiers', book_ids, default_value={})
                data[field] = {k: v.get('isbn') or '' for k, v in iteritems(x)}
                continue
            field = field.replace('*', '#')
            metadata[field] = fm[field]
            if not is_remote:
                if field == 'formats':
                    data[field] = {k: list(formats(db, k)) for k in book_ids}
                    continue
                if field == 'cover':
                    data[field] = {k: cover(db, k) for k in book_ids}
                    continue
            data[field] = db.all_field_for(field, book_ids)
    return {'book_ids': book_ids, "data": data, 'metadata': metadata, 'fields':fields}


def stringify(data, metadata, for_machine):
    for field, m in iteritems(metadata):
        if field == 'authors':
            data[field] = {
                k: authors_to_string(v)
                for k, v in iteritems(data[field])
            }
        else:
            dt = m['datatype']
            if dt == 'datetime':
                data[field] = {
                    k: isoformat(v, as_utc=for_machine) if v else 'None'
                    for k, v in iteritems(data[field])
                }
            elif not for_machine:
                ism = m['is_multiple']
                if ism:
                    data[field] = {
                        k: ism['list_to_ui'].join(v)
                        for k, v in iteritems(data[field])
                    }
                    if field == 'formats':
                        data[field] = {
                            k: '[' + v + ']'
                            for k, v in iteritems(data[field])
                        }


def as_machine_data(book_ids, data, metadata):
    for book_id in book_ids:
        ans = {'id': book_id}
        for field, val_map in iteritems(data):
            val = val_map.get(book_id)
            if val is not None:
                ans[field.replace('#', '*')] = val
        yield ans


def prepare_output_table(fields, book_ids, data, metadata):
    ans = []
    for book_id in book_ids:
        row = []
        ans.append(row)
        for field in fields:
            if field == 'id':
                row.append(str(book_id))
                continue
            val = data.get(field.replace('*', '#'), {}).get(book_id)
            row.append(str(val).replace('\n', ' '))
    return ans


def do_list(
    dbctx,
    fields,
    afields,
    sort_by,
    ascending,
    search_text,
    line_width,
    separator,
    prefix,
    limit,
    for_machine=False
):
    if sort_by is None:
        ascending = True
    ans = dbctx.run('list', fields, sort_by, ascending, search_text, limit)
    try:
        book_ids, data, metadata = ans['book_ids'], ans['data'], ans['metadata']
    except TypeError:
        raise SystemExit(ans)
    fields = list(ans['fields'])
    try:
        fields.remove('id')
    except ValueError:
        pass
    fields = ['id'] + fields
    stringify(data, metadata, for_machine)
    if for_machine:
        raw = json.dumps(
            list(as_machine_data(book_ids, data, metadata)),
            indent=2,
            sort_keys=True
        )
        if not isinstance(raw, bytes):
            raw = raw.encode('utf-8')
        getattr(sys.stdout, 'buffer', sys.stdout).write(raw)
        return
    from calibre.utils.terminal import ColoredStream, geometry

    output_table = prepare_output_table(fields, book_ids, data, metadata)
    widths = list(map(lambda x: 0, fields))

    for record in output_table:
        for j in range(len(fields)):
            widths[j] = max(widths[j], str_width(record[j]))

    screen_width = geometry()[0] if line_width < 0 else line_width
    if not screen_width:
        screen_width = 80
    field_width = screen_width // len(fields)
    base_widths = list(map(lambda x: min(x + 1, field_width), widths))

    while sum(base_widths) < screen_width:
        adjusted = False
        for i in range(len(widths)):
            if base_widths[i] < widths[i]:
                base_widths[i] += min(
                    screen_width - sum(base_widths), widths[i] - base_widths[i]
                )
                adjusted = True
                break
        if not adjusted:
            break

    widths = list(base_widths)
    titles = map(
        lambda x, y: '%-*s%s' % (x - len(separator), y, separator), widths,
        fields
    )
    with ColoredStream(sys.stdout, fg='green'):
        print(''.join(titles), flush=True)
    stdout = getattr(sys.stdout, 'buffer', sys.stdout)
    linesep = as_bytes(os.linesep)

    wrappers = [TextWrapper(x - 1).wrap if x > 1 else lambda y: y for x in widths]

    for record in output_table:
        text = [
            wrappers[i](record[i]) for i, field in enumerate(fields)
        ]
        lines = max(map(len, text))
        for l in range(lines):
            for i, field in enumerate(text):
                ft = text[i][l] if l < len(text[i]) else ''
                stdout.write(ft.encode('utf-8'))
                if i < len(text) - 1:
                    filler = ('%*s' % (widths[i] - str_width(ft) - 1, ''))
                    stdout.write((filler + separator).encode('utf-8'))
            stdout.write(linesep)


def option_parser(get_parser, args):
    parser = get_parser(
        _(
            '''\
%prog list [options]

List the books available in the calibre database.
'''
        )
    )
    parser.add_option(
        '-f',
        '--fields',
        default='title,authors',
        help=_(
            'The fields to display when listing books in the'
            ' database. Should be a comma separated list of'
            ' fields.\nAvailable fields: %s\nDefault: %%default. The'
            ' special field "all" can be used to select all fields.'
            ' In addition to the builtin fields above, custom fields are'
            ' also available as *field_name, for example, for a custom field'
            ' #rating, use the name: *rating'
        ) % ', '.join(sorted(FIELDS))
    )
    parser.add_option(
        '--sort-by',
        default=None,
        help=_(
            'The field by which to sort the results.\nAvailable fields: {0}\nDefault: {1}'
        ).format(', '.join(sorted(FIELDS)), 'id')
    )
    parser.add_option(
        '--ascending',
        default=False,
        action='store_true',
        help=_('Sort results in ascending order')
    )
    parser.add_option(
        '-s',
        '--search',
        default=None,
        help=_(
            'Filter the results by the search query. For the format of the search query,'
            ' please see the search related documentation in the User Manual. Default is to do no filtering.'
        )
    )
    parser.add_option(
        '-w',
        '--line-width',
        default=-1,
        type=int,
        help=_(
            'The maximum width of a single line in the output. Defaults to detecting screen size.'
        )
    )
    parser.add_option(
        '--separator',
        default=' ',
        help=_('The string used to separate fields. Default is a space.')
    )
    parser.add_option(
        '--prefix',
        default=None,
        help=_(
            'The prefix for all file paths. Default is the absolute path to the library folder.'
        )
    )
    parser.add_option(
        '--limit',
        default=-1,
        type=int,
        help=_('The maximum number of results to display. Default: all')
    )
    parser.add_option(
        '--for-machine',
        default=False,
        action='store_true',
        help=_(
            'Generate output in JSON format, which is more suitable for machine parsing. Causes the line width and separator options to be ignored.'
        )
    )
    return parser


def main(opts, args, dbctx):
    afields = set(FIELDS) | {'id'}
    if opts.fields.strip():
        fields = [str(f.strip().lower()) for f in opts.fields.split(',')]
    else:
        fields = []

    do_list(
        dbctx,
        fields,
        afields,
        opts.sort_by,
        opts.ascending,
        opts.search,
        opts.line_width,
        opts.separator,
        opts.prefix,
        opts.limit,
        for_machine=opts.for_machine
    )
    return 0
