#!/usr/local/bin/python3.12


__license__   = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import ast
import json
import os

from calibre import sanitize_file_name
from calibre.customize.conversion import OptionRecommendation
from calibre.customize.ui import available_output_formats
from calibre.utils.config import config_dir, prefs, tweaks
from calibre.utils.lock import ExclusiveFile

config_dir = os.path.join(config_dir, 'conversion')


def name_to_path(name):
    return os.path.join(config_dir, sanitize_file_name(name)+'.py')


def save_defaults(name, recs):
    path = name_to_path(name)
    raw = recs.serialize()

    os.makedirs(config_dir, exist_ok=True)

    with open(path, 'wb'):
        pass
    with ExclusiveFile(path) as f:
        f.write(raw)


def load_defaults(name):
    path = name_to_path(name)

    os.makedirs(config_dir, exist_ok=True)

    if not os.path.exists(path):
        open(path, 'wb').close()
    with ExclusiveFile(path) as f:
        raw = f.read()
    r = GuiRecommendations()
    if raw:
        r.deserialize(raw)
    return r


def save_specifics(db, book_id, recs):
    raw = recs.serialize()
    db.new_api.set_conversion_options({book_id: raw}, fmt='PIPE')


def load_specifics(db, book_id):
    raw = db.conversion_options(book_id, 'PIPE')
    r = GuiRecommendations()
    if raw:
        r.deserialize(raw)
    return r


def delete_specifics(db, book_id):
    db.delete_conversion_options(book_id, 'PIPE')


class GuiRecommendations(dict):

    def __new__(cls, *args):
        dict.__new__(cls)
        obj = super().__new__(cls, *args)
        obj.disabled_options = set()
        return obj

    def to_recommendations(self, level=OptionRecommendation.LOW):
        ans = []
        for key, val in self.items():
            ans.append((key, val, level))
        return ans

    def __str__(self):
        ans = ['{']
        for key, val in self.items():
            ans.append('\t'+repr(key)+' : '+repr(val)+',')
        ans.append('}')
        return '\n'.join(ans)

    def serialize(self):
        ans = json.dumps(self, indent=2, ensure_ascii=False)
        if isinstance(ans, str):
            ans = ans.encode('utf-8')
        return b'json:' + ans

    def deserialize(self, raw):
        try:
            if raw.startswith(b'json:'):
                d = json.loads(raw[len(b'json:'):])
            else:
                d = ast.literal_eval(raw)
        except Exception:
            pass
        else:
            if d:
                self.update(d)
    from_string = deserialize

    def merge_recommendations(self, get_option, level, options,
            only_existing=False):
        for name in options:
            if only_existing and name not in self:
                continue
            opt = get_option(name)
            if opt is None:
                continue
            if opt.level == OptionRecommendation.HIGH:
                self[name] = opt.recommended_value
                self.disabled_options.add(name)
            elif opt.level > level or name not in self:
                self[name] = opt.recommended_value

    def as_dict(self):
        return {
            'options': self.copy(),
            'disabled': tuple(self.disabled_options)
        }


def get_available_formats_for_book(db, book_id):
    available_formats = db.new_api.formats(book_id)
    return {x.lower() for x in available_formats}


class NoSupportedInputFormats(Exception):

    def __init__(self, available_formats):
        Exception.__init__(self)
        self.available_formats = available_formats


def get_supported_input_formats_for_book(db, book_id):
    from calibre.ebooks.conversion.plumber import supported_input_formats
    available_formats = get_available_formats_for_book(db, book_id)
    input_formats = {x.lower() for x in supported_input_formats()}
    input_formats = sorted(available_formats.intersection(input_formats))
    if not input_formats:
        raise NoSupportedInputFormats(tuple(x for x in available_formats if x))
    return input_formats


def get_preferred_input_format_for_book(db, book_id):
    recs = load_specifics(db, book_id)
    if recs:
        return recs.get('gui_preferred_input_format', None)


def sort_formats_by_preference(formats, prefs):
    uprefs = {x.upper():i for i, x in enumerate(prefs)}

    def key(x):
        return uprefs.get(x.upper(), len(prefs))

    return sorted(formats, key=key)


def get_input_format_for_book(db, book_id, pref=None):
    '''
    Return (preferred input format, list of available formats) for the book
    identified by book_id. Raises an error if the book has no input formats.

    :param pref: If None, the format used as input for the last conversion, if
    any, on this book is used. If not None, should be a lowercase format like
    'epub' or 'mobi'. If you do not want the last converted format to be used,
    set pref=False.
    '''
    if pref is None:
        pref = get_preferred_input_format_for_book(db, book_id)
    if hasattr(pref, 'lower'):
        pref = pref.lower()
    input_formats = get_supported_input_formats_for_book(db, book_id)
    input_format = pref if pref in input_formats else \
        sort_formats_by_preference(
            input_formats, prefs['input_format_order'])[0]
    return input_format, input_formats


def get_output_formats(preferred_output_format):
    all_formats = {x.upper() for x in available_output_formats()}
    all_formats.discard('OEB')
    pfo = preferred_output_format.upper() if preferred_output_format else ''
    restrict = tweaks['restrict_output_formats']
    if restrict:
        fmts = [x.upper() for x in restrict]
        if pfo and pfo not in fmts and pfo in all_formats:
            fmts.append(pfo)
    else:
        fmts = list(sorted(all_formats,
            key=lambda x:{'EPUB':'!A', 'AZW3':'!B', 'MOBI':'!C'}.get(x.upper(), x)))
    return fmts


def get_sorted_output_formats(preferred_fmt=None):
    preferred_output_format = (preferred_fmt or prefs['output_format']).upper()
    fmts = get_output_formats(preferred_output_format)
    try:
        fmts.remove(preferred_output_format)
    except Exception:
        pass
    fmts.insert(0, preferred_output_format)
    return fmts


OPTIONS = {
    'input': {
        'comic': (
            'colors', 'dont_normalize', 'keep_aspect_ratio', 'right2left', 'despeckle', 'no_sort', 'no_process', 'landscape',
            'dont_sharpen', 'disable_trim', 'wide', 'output_format', 'dont_grayscale', 'comic_image_size', 'dont_add_comic_pages_to_toc'),

        'docx': ('docx_no_cover', 'docx_no_pagebreaks_between_notes', 'docx_inline_subsup'),

        'fb2': ('no_inline_fb2_toc',),

        'pdf': ('no_images', 'unwrap_factor', 'pdf_engine', 'pdf_header_skip', 'pdf_footer_skip', 'pdf_header_regex', 'pdf_footer_regex'),

        'rtf': ('ignore_wmf',),

        'txt': ('paragraph_type', 'formatting_type', 'markdown_extensions', 'preserve_spaces', 'txt_in_remove_indents'),
    },

    'pipe': {
        'debug': ('debug_pipeline',),

        'heuristics': (
            'enable_heuristics', 'markup_chapter_headings',
            'italicize_common_cases', 'fix_indents', 'html_unwrap_factor',
            'unwrap_lines', 'delete_blank_paragraphs', 'format_scene_breaks',
            'replace_scene_breaks', 'dehyphenate', 'renumber_headings'),

        'look_and_feel': (
            'change_justification', 'extra_css', 'base_font_size',
            'font_size_mapping', 'line_height', 'minimum_line_height',
            'embed_font_family', 'embed_all_fonts', 'subset_embedded_fonts',
            'smarten_punctuation', 'unsmarten_punctuation',
            'disable_font_rescaling', 'insert_blank_line',
            'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size',
            'insert_blank_line_size', 'input_encoding', 'filter_css',
            'expand_css', 'asciiize', 'keep_ligatures', 'linearize_tables',
            'transform_css_rules', 'transform_html_rules'),

        'metadata': ('prefer_metadata_cover',),

        'page_setup': (
            'margin_top', 'margin_left', 'margin_right', 'margin_bottom',
            'input_profile', 'output_profile'),

        'search_and_replace': (
            'search_replace', 'sr1_search', 'sr1_replace', 'sr2_search', 'sr2_replace', 'sr3_search', 'sr3_replace'),

        'structure_detection': (
            'chapter', 'chapter_mark', 'start_reading_at',
            'remove_first_image', 'remove_fake_margins', 'insert_metadata',
            'page_breaks_before', 'add_alt_text_to_img',),

        'toc': (
            'level1_toc', 'level2_toc', 'level3_toc',
            'toc_threshold', 'max_toc_links', 'no_chapters_in_toc',
            'use_auto_toc', 'toc_filter', 'duplicate_links_in_toc',),
    },

    'output': {
        'azw3': ('prefer_author_sort', 'toc_title', 'mobi_toc_at_start', 'dont_compress', 'no_inline_toc', 'share_not_sync',),

        'docx': (
            'docx_page_size', 'docx_custom_page_size', 'docx_no_cover', 'docx_no_toc',
            'docx_page_margin_left', 'docx_page_margin_top', 'docx_page_margin_right',
            'docx_page_margin_bottom', 'preserve_cover_aspect_ratio',),

        'epub': (
            'dont_split_on_page_breaks', 'flow_size', 'no_default_epub_cover',
            'no_svg_cover', 'epub_inline_toc', 'epub_toc_at_end', 'toc_title',
            'preserve_cover_aspect_ratio', 'epub_flatten', 'epub_version', 'epub_max_image_size',),

        'kepub': (
            'dont_split_on_page_breaks', 'flow_size', 'kepub_max_image_size',
            'kepub_affect_hyphenation', 'kepub_disable_hyphenation', 'kepub_hyphenation_min_chars',
            'kepub_hyphenation_min_chars_before', 'kepub_hyphenation_min_chars_after', 'kepub_hyphenation_limit_lines',
        ),

        'fb2': ('sectionize', 'fb2_genre'),

        'htmlz': ('htmlz_css_type', 'htmlz_class_style', 'htmlz_title_filename'),

        'lrf': (
            'wordspace', 'header', 'header_format', 'minimum_indent',
            'serif_family', 'render_tables_as_images', 'sans_family',
            'mono_family', 'text_size_multiplier_for_rendered_tables',
            'autorotation', 'header_separation', 'minimum_indent'),

        'mobi': (
            'prefer_author_sort', 'toc_title', 'mobi_keep_original_images',
            'mobi_ignore_margins', 'mobi_toc_at_start', 'dont_compress',
            'no_inline_toc', 'share_not_sync', 'personal_doc',
            'mobi_file_type'),

        'pdb': ('format', 'inline_toc', 'pdb_output_encoding'),

        'pdf': (
            'use_profile_size', 'paper_size', 'custom_size', 'pdf_hyphenate',
            'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit',
            'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font',
            'pdf_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers',
            'pdf_footer_template', 'pdf_header_template', 'pdf_add_toc',
            'toc_title', 'pdf_page_margin_left', 'pdf_page_margin_top',
            'pdf_page_margin_right', 'pdf_page_margin_bottom', 'pdf_no_cover',
            'pdf_use_document_margins', 'pdf_page_number_map', 'pdf_odd_even_offset'),

        'pml': ('inline_toc', 'full_image_depth', 'pml_output_encoding'),

        'rb': ('inline_toc',),

        'snb': (
            'snb_insert_empty_line', 'snb_dont_indent_first_line',
            'snb_hide_chapter_name','snb_full_screen'),

        'txt': (
            'newline', 'max_line_length', 'force_max_line_length',
            'inline_toc', 'txt_output_formatting', 'keep_links', 'keep_image_references',
            'keep_color', 'txt_output_encoding'),
    },
}
OPTIONS['output']['txtz'] = OPTIONS['output']['txt']


def options_for_input_fmt(fmt):
    from calibre.customize.ui import plugin_for_input_format
    fmt = fmt.lower()
    plugin = plugin_for_input_format(fmt)
    if plugin is None:
        return None, ()
    full_name = plugin.name.lower().replace(' ', '_')
    name = full_name.rpartition('_')[0]
    return full_name, OPTIONS['input'].get(name, ())


def options_for_output_fmt(fmt):
    from calibre.customize.ui import plugin_for_output_format
    fmt = fmt.lower()
    plugin = plugin_for_output_format(fmt)
    if plugin is None:
        return None, ()
    full_name = plugin.name.lower().replace(' ', '_')
    name = full_name.rpartition('_')[0]
    return full_name, OPTIONS['output'].get(name, ())
