# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals

from uuid import short_uuid

from ajax import ajax_send

all_settings = {
    'copy_to_library_dupes': {'default': 'add;overwrite', 'category': 'book_list'},
    'last_sort_order': {'default': {}, 'category': 'book_list'},
    'show_all_metadata': {'default': False, 'category': 'book_list'},  # show all metadata fields in the book details panel
    'show_nav_list_book_count': {'default': True, 'category': 'book_list', 'is_local': False},
    'show_nav_list_avg_rating': {'default': True, 'category': 'book_list', 'is_local': False},
    'show_sidebar_in_book_viewer': {'default': False, 'category': 'book_list', 'is_local': False},
    # auto = follow OS; dark/light = force content-server chrome (book list, sidebar, modals).
    'book_list_color_scheme': {'default': 'auto', 'category': 'book_list', 'is_local': False},
    'ui_highlight_colors': {'default': {'light': '#437000', 'dark': '#6cb4ee'}, 'category': 'book_list', 'is_local': False},
    'series_nav_display_mode': {'default': 'stack', 'category': 'book_list', 'is_local': False},
    'saved_virtual_libraries': {'default': [], 'category': 'book_list', 'is_local': False},
    'browse_visible_fields': {'default': {}, 'category': 'book_list', 'is_local': False},
    'sort': {'default': 'timestamp.desc', 'category': 'book_list'},  # comma separated list of items of the form: field.order
    'view_mode': {'default': 'cover_grid', 'category': 'book_list'},
    # Cover grid tile width as % of viewport axis (portrait: vw, landscape: vh), integer ~8–45.
    'cover_grid_tile_pct': {'default': 17, 'category': 'book_list'},
    'fts_related_words': {'default': True, 'category': 'book_list'},
    # Tag Browser settings
    'and_search_terms': {'default': False, 'category': 'tag_browser'},  # how to add search terms to the search expression from the Tag Browser
    'collapse_at': {'default': 25, 'category': 'tag_browser'},  # number of items at which sub-groups are created, 0 to disable
    'dont_collapse': {'default': '', 'category': 'tag_browser'},  # comma separated list of category names
    'hide_empty_categories': {'default': 'no', 'category': 'tag_browser'},
    'partition_method': {'default': 'first letter', 'category': 'tag_browser'},  # other choices: 'disable', 'partition'
    'sort_tags_by': {'default': 'name', 'category': 'tag_browser'},  # other choices: popularity, rating
    # Book reader settings
    'background_image_fade': {'default': 0, 'category': 'read_book', 'is_local': True},
    'background_image_style': {'default': 'scaled', 'category': 'read_book', 'is_local': True},
    'background_image': {'default': None, 'category': 'read_book', 'is_local': True},
    'base_font_size': {'default': 16, 'category': 'read_book', 'is_local': True},
    'book_scrollbar': {'default': False, 'category': 'read_book'},
    'columns_per_screen': {'default': {'portrait': 0, 'landscape': 0}, 'category': 'read_book', 'is_local': True},
    'controls_help_shown_count': {'default': 0, 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
    'controls_help_shown_count_rtl_page_progression': {'default': 0, 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
    'cover_preserve_aspect_ratio': {'default': True, 'category': 'read_book'},
    'current_color_scheme': {'default': 'system', 'category': 'read_book'},
    'footer': {'default': {'left': 'menu-icon-on-hover', 'right': 'progress'}, 'category': 'read_book'},
    'header': {'default': {'right': 'menu-icon-on-hover'}, 'category': 'read_book'},
    'controls_footer': {'default': {'right': 'progress'}, 'category': 'read_book'},
    'left-margin': {'default': {}, 'category': 'read_book'},
    'right-margin': {'default': {}, 'category': 'read_book'},
    'hide_tooltips': {'default': False, 'category': 'read_book'},
    'keyboard_shortcuts': {'default': {}, 'category': 'read_book'},
    'lines_per_sec_auto': {'default': 1, 'category': 'read_book', 'is_local': True},
    'lines_per_sec_smooth': {'default': 20, 'category': 'read_book', 'is_local': True},
    'margin_bottom': {'default': 20, 'category': 'read_book', 'is_local': True},
    'margin_left': {'default': 20, 'category': 'read_book', 'is_local': True},
    'margin_right': {'default': 20, 'category': 'read_book', 'is_local': True},
    'margin_top': {'default': 20, 'category': 'read_book', 'is_local': True},
    'max_text_height': {'default': 0, 'category': 'read_book', 'is_local': True},
    'max_text_width': {'default': 0, 'category': 'read_book', 'is_local': True},
    'override_book_colors': {'default': 'never', 'category': 'read_book'},
    'paged_margin_clicks_scroll_by_screen': {'default': True, 'category': 'read_book'},
    'paged_wheel_scrolls_by_screen': {'default': False, 'category': 'read_book'},
    'paged_wheel_section_jumps': {'default': True, 'category': 'read_book'},
    'paged_pixel_scroll_threshold': {'default': 60, 'category': 'read_book'},
    'read_mode': {'default': 'paged', 'category': 'read_book', 'is_local': True},
    'scroll_auto_boundary_delay': {'default': 5, 'category': 'read_book', 'is_local': True},
    'scroll_mouse_boundary_delay': {'default': 0.5, 'category': 'read_book', 'is_local': True},
    'scroll_stop_boundaries': {'default': False, 'category': 'read_book', 'is_local': True},
    'standalone_font_settings': {'default': {}, 'category': 'read_book', 'is_local': True},
    'standalone_misc_settings': {'default': {}, 'category': 'read_book', 'is_local': True},
    'standalone_recently_opened': {'default': [], 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
    'user_color_schemes': {'default': {}, 'category': 'read_book'},
    'user_stylesheet': {'default': '', 'category': 'read_book', 'is_local': True},
    'word_actions': {'default': [], 'category': 'read_book'},
    'highlight_style': {'default': None, 'category': 'read_book', 'is_local': True},
    'highlights_export_format': {'default': 'text', 'category': 'read_book', 'is_local': True},
    'custom_highlight_styles': {'default': [], 'category': 'read_book'},
    'show_selection_bar': {'default': True, 'category': 'read_book'},
    'net_search_url': {'default': 'https://google.com/search?q={q}', 'category': 'read_book'},
    'selection_bar_actions': {'default': ['copy', 'lookup', 'highlight', 'remove_highlight', 'search_net', 'clear'], 'category': 'read_book'},
    'selection_bar_quick_highlights': {'default': [], 'category': 'read_book'},
    'skipped_dialogs': {'default': v'{}', 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
    'tts': {'default': v'{}', 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
    'tts_backend': {'default': v'{}', 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
    'fullscreen_when_opening': {'default': 'auto', 'category': 'read_book', 'is_local': True},
    'book_search_mode': {'default': 'contains', 'category': 'read_book', 'is_local': True},
    'book_search_case_sensitive': {'default': False, 'category': 'read_book', 'is_local': True},
    'reverse_page_turn_zones': {'default': False, 'category': 'read_book', 'is_local': True},
    'gesture_overrides': {'default': {}, 'category': 'read_book'},
    'cite_text_template': {'default': '[{text}]({url})', 'category': 'read_book'},
    'cite_hl_template': {'default': '[{text}]({url})', 'category': 'read_book'},
    'tts_bar_position': {'default': 'float', 'category': 'read_book', 'is_local': False, 'disallowed_in_profile': False},
}

defaults = {}
for x in Object.entries(all_settings):
    defaults[x[0]] = x[1].default


def session_defaults():
    return defaults


def storage_available(which):
    which = which or 'localStorage'
    try:
        storage = window[which]
        x = '__storage__test__'
        storage.setItem(x, x)
        storage.removeItem(x)
        return True
    except:
        return False


class FakeStorage:
    def __init__(self):
        self.data = {}

    def getItem(self, key):
        return self.data[key]

    def setItem(self, key, value):
        if jstype(value) is not 'string':
            value = JSON.stringify(value)
        self.data[key] = value

    def clear(self):
        self.data = {}


def get_session_storage():
    if not get_session_storage.ans:
        if storage_available('localStorage'):
            get_session_storage.ans = window.localStorage
        elif storage_available('sessionStorage'):
            get_session_storage.ans = window.sessionStorage
            console.error('localStorage not available using sessionStorage instead')
        else:
            get_session_storage.ans = FakeStorage()
            console.error('sessionStorage and localStorage not available using a temp cache instead')
    return get_session_storage.ans


class SessionData:
    def __init__(self, global_prefix=None):
        self.global_prefix = global_prefix or 'calibre-session-'
        self.storage = get_session_storage()
        self.overflow_storage = {}
        self.has_overflow = False

    def get(self, key, defval):
        key = self.global_prefix + key
        if self.has_overflow:
            ans = self.overflow_storage[key]
            if ans is undefined:
                ans = self.storage.getItem(key)
        else:
            ans = self.storage.getItem(key)
        if ans is undefined or ans is None:
            if defval is undefined:
                defval = None
            return defval
        try:
            return JSON.parse(ans)
        except:
            if defval is undefined:
                defval = None
            return defval

    def set(self, key, value):
        key = self.global_prefix + key
        if value is None:
            self.storage.removeItem(key)
            v'delete self.overflow_storage[key]'
            return True
        value = JSON.stringify(value)
        try:
            self.storage.setItem(key, value)
            v'delete self.overflow_storage[key]'
            return True
        except:
            self.overflow_storage[key] = value
            self.has_overflow = True
            console.error('session storage has overflowed, using a temp cache instead')
        return False

    def set_bulk(self, changes):
        for key in Object.keys(changes):
            self.set(key, changes[key])

    def clear(self):
        self.storage.clear()
        self.overflow_storage = {}
        self.has_overflow = False


def local_storage():
    if not local_storage.storage:
        local_storage.storage = SessionData('calibre-local-')
    return local_storage.storage


def get_device_uuid():
    if not get_device_uuid.ans:
        s = local_storage()
        ans = s.get('device_uuid')
        if not ans:
            ans = short_uuid()
            s.set('device_uuid', ans)
        get_device_uuid.ans = ans
    return get_device_uuid.ans


def deep_eq(a, b):
    if a is b:
        return True
    if Array.isArray(a) and Array.isArray(b):
        if a.length is not b.length:
            return False
        for i in range(a.length):
            if not deep_eq(a[i], b[i]):
                return False
        return True
    if a is None or b is None or a is undefined or b is undefined:
        return False
    if jstype(a) is not 'object' or jstype(b) is not 'object':
        return False
    ka = Object.keys(a)
    if ka.length is not Object.keys(b).length:
        return False
    for key in ka:
        if not deep_eq(a[key], b[key]):
            return False
    return True


def setting_val_is_default(setting_name, curval):
    defval = defaults[setting_name]
    return deep_eq(defval, curval)


def get_subset_of_settings(sd, filter_func):
    ans = {}
    for setting_name in Object.keys(all_settings):
        curval = sd.get(setting_name, ans)
        metadata = all_settings[setting_name]
        is_set = curval is not ans
        if filter_func(setting_name, metadata, is_set, curval):
            ans[setting_name] = window.structuredClone(curval if is_set else metadata.default)
    return ans


def apply_profile(sd, profile, filter_func):
    changes = {}
    for setting_name in Object.keys(all_settings):
        metadata = all_settings[setting_name]
        curval = sd.get(setting_name, changes)
        is_set = curval is not changes
        if filter_func(setting_name, metadata, is_set, curval):
            newval = None
            if Object.prototype.hasOwnProperty.call(profile, setting_name):
                newval = profile[setting_name]
                if deep_eq(newval, metadata.default):
                    newval = None
            changes[setting_name] = window.structuredClone(newval)
    sd.set_bulk(changes)
    return changes


standalone_reader_defaults = {
    'remember_window_geometry': False,
    'remember_last_read': True,
    'show_actions_toolbar': False,
    'show_actions_toolbar_in_fullscreen': False,
    'save_annotations_in_ebook': True,
    'sync_annots_user': '',
    'singleinstance': False,
    'auto_hide_mouse': True,
    'restore_docks': True,
}


def settings_for_reader_profile(sd):
    margin_text_settings = {'footer': True, 'header': True, 'controls_footer': True, 'left-margin': True, 'right-margin': True}

    def filter_func(setting_name, metadata, is_set, curval):
        if margin_text_settings[setting_name]:
            curval = window.structuredClone(curval)
            for x in ['left', 'right', 'middle']:
                if curval[x] is 'empty':
                    v'delete curval[x]'
        elif setting_name is 'standalone_misc_settings':
            curval = window.structuredClone(curval)
            for key in Object.keys(curval):
                if curval[key] is standalone_reader_defaults[key]:
                    v'delete curval[key]'

        if not is_set or metadata.category is not 'read_book' or metadata.disallowed_in_profile or deep_eq(curval, metadata.default):
            return False
        return True

    return get_subset_of_settings(sd, filter_func)


def apply_reader_profile(sd, profile):
    def filter_func(setting_name, metadata, is_set, curval):
        return metadata.category is 'read_book' and not metadata.disallowed_in_profile
    apply_profile(sd, profile, filter_func)


default_interface_data = {
    'username': None,
    'output_format': 'EPUB',
    'input_formats': {'EPUB', 'MOBI', 'AZW3'},
    'gui_pubdate_display_format': 'MMM yyyy',
    'gui_timestamp_display_format': 'dd MMM yyyy',
    'gui_last_modified_display_format': 'dd MMM yyyy',
    'use_roman_numerals_for_series_number': True,
    'default_library_id': None,
    'default_book_list_mode': session_defaults().view_mode,
    'library_map': None,
    'search_the_net_urls': [],
    'donate_link': 'https://calibre-ebook.com/donate',
    'default_author_link': '',
    'icon_map': {},
    'icon_path': '',
    'custom_list_template': None,
    'num_per_page': 50,
    'lang_code_for_user_manual': '',
}


def get_interface_data():
    if not get_interface_data.storage:
        get_interface_data.storage = SessionData('calibre-interface-data-')
    ans = get_interface_data.storage.get('current')
    if ans:
        ans.is_default = False
    else:
        ans = {'is_default': True}
        for k in default_interface_data:
            ans[k] = default_interface_data[k]
    return ans


def update_interface_data(new_data):
    data = get_interface_data()
    for k in default_interface_data:
        nval = new_data[k]
        if k is not undefined:
            data[k] = nval
    if not get_interface_data.storage:
        get_interface_data.storage = SessionData('calibre-interface-data-')
    get_interface_data.storage.set('current', data)


def get_translations(newval):
    if not get_translations.storage:
        get_translations.storage = SessionData('calibre-translations-')
    if newval?:
        get_translations.storage.set('current', newval)
    else:
        return get_translations.storage.get('current')


def is_setting_local(name):
    m = all_settings[name]
    return m.is_local if m else True


class UserSessionData(SessionData):
    def __init__(self, username, saved_data):
        self.prefix = (username or '') + '-'
        self.has_user = bool(username)
        self.username = username
        SessionData.__init__(self)
        self.echo_changes = False
        self.changes = {}
        self.has_changes = False
        self.push_timer_id = None
        if saved_data:
            for key in saved_data:
                if not is_setting_local(key):
                    self.set(key, saved_data[key])
        self.echo_changes = True

    def defval(self, key):
        ans = session_defaults()[key]
        if ans:
            ans = window.structuredClone(ans)
        return ans

    def get(self, key, defval):
        if defval is undefined:
            defval = self.defval(key)
        return SessionData.get(self, (self.prefix + key), defval)

    def get_library_option(self, library_id, key, defval):
        if not library_id:
            return self.get(key, defval)
        lkey = key + '-||-' + library_id
        if defval is undefined:
            defval = self.defval(key)
        return self.get(lkey, defval)

    def set(self, key, value):
        if self.echo_changes and self.has_user and not is_setting_local(key):
            self.changes[key] = value
            self.has_changes = True
            if self.push_timer_id is not None:
                clearTimeout(self.push_timer_id)
            self.push_timer_id = setTimeout(self.push_to_server.bind(self), 1000)
        return SessionData.set(self, (self.prefix + key), value)

    def set_library_option(self, library_id, key, value):
        if library_id:
            key = key + '-||-' + library_id
        return self.set(key, value)

    def push_to_server(self):
        if self.has_changes:
            ajax_send('interface-data/set-session-data', self.changes, def (end_type, xhr, ev):
                if end_type is not 'load':
                    console.error('Failed to send session data to server: ' + xhr.error_html)
            )
            self.changes = {}
            self.has_changes = False
