#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai

from __future__ import (unicode_literals, division, absolute_import, print_function)

import math
import datetime
import functools

from calibre.utils.config_base import tweaks

from calibre_plugins.overdrive_link.tweak import TWEAK_AVG_LOAN_WEEKS

from .python_transition import (IS_PYTHON2)
if IS_PYTHON2:
    from .python_transition import (str)

__license__ = 'GPL v3'
__copyright__ = '2012-2025, John Howell <jhowell@acm.org>'


provider_by_id_ = {}     # index the subclasses of SearchableLibrary
provider_by_name_ = {}   # index the subclasses of SearchableLibrary

AVG_LOAN_WEEKS = 2.5
NO_WAIT_WEEKS = 0
MAX_WAIT_WEEKS = 99
SECONDS_PER_WEEK = 60 * 60 * 24 * 7


class LendingLibrary:
    '''
    Information about a provider of lending library services
    Includes attributes from a LibraryConfig  namedtuple
    priority (0..n) - From low to high, controls the order in which libraries and links to them are displayed
    '''

    def __init__(self, **kwargs):

        self.__dict__.update(kwargs)        # include attributes from LibraryConfig & priority

        self.provider = SearchableLibrary.provider(self.provider_id)
        self.library_id = self.provider.validate_library_id(self.library_id, config=None)
        self.branch_id = self.provider.validate_branch_id(self.branch_id)

        self.full_library_id = full_library_id(self.provider_id, self.library_id)


@functools.total_ordering
class SearchableLibrary:
    '''
    Only default values, static and class methods can be accessed outside of a search job by provider
    '''

    # defaults
    supports_recommendation = False
    is_amazon = False
    is_project_gutenberg = False
    allow_format_merge = False          # error if formats may differ for same book at different libraries
    sign_in_affects_get_current_availability = False
    title_used_in_search = True
    formats_supported = set()

    @classmethod
    def register(cls):
        #print('register provider id: "%s" to %s' % (cls.id, str(cls)))
        global provider_by_id_
        global provider_by_name_

        provider_by_id_[cls.id] = cls
        provider_by_name_[cls.name] = cls

    @staticmethod
    def validate_provider_id(provider_id):
        global provider_by_id_
        if provider_id not in provider_by_id_:
            raise ValueError('Unknown provider id: %s' % provider_id)

    @staticmethod
    def provider(provider_id):
        global provider_by_id_
        return provider_by_id_.get(provider_id, SearchableLibrary)

    @staticmethod
    def provider_by_name(name):
        global provider_by_name_

        try:
            return provider_by_name_[name]
        except Exception:
            raise ValueError('Unknown provider name: %s' % name)

    @staticmethod
    def provider_names():
        global provider_by_name_

        return list(provider_by_name_.keys())

    @staticmethod
    def create(log, config, lending_lib):
        # create a SearchableLibrary based on a LendingLibrary

        SearchableLibrary.validate_provider_id(lending_lib.provider_id)
        lib = SearchableLibrary.provider(lending_lib.provider_id)()     # create and init based on provider type

        lib.__dict__.update(lending_lib.__dict__)    # include fields from LendingLibrary
        lib.log = log
        lib.config = config

        return lib

    @staticmethod
    def validate_library_id(library_id, migrate=True, config=None):
        # raise exception for invalid id. Correct and return new value for minor problems.
        return library_id

    @staticmethod
    def validate_branch_id(branch_id):
        # raise exception for invalid id. Correct and return new value for minor problems.
        return branch_id

    @staticmethod
    def validate_book_id(book_id, library_id):
        # raise exception for invalid id. Correct and return new value for minor problems.
        return book_id

    @staticmethod
    def book_url(library_id, book_id):
        # return the URL to access the book at the library
        return ''

    @staticmethod
    def book_key_library_id(library_id):
        return ''   # assume has same book ids and available formats at all libraries

    @staticmethod
    def amazon_ident(library_id):
        raise ValueError('amazon_ident: provider is not amazon')

    @staticmethod
    def supports_purchase(library_id):
        return False

    def __hash__(self):
        return hash((self.provider_id, self.library_id))

    def __eq__(self, other):
        return (self.provider_id == other.provider_id) and (self.library_id == other.library_id)

    def __lt__(self, other):
        return (self.provider_id, self.library_id) < (other.provider_id, other.library_id)

    def __repr__(self):
        return full_library_id(self.provider_id, self.library_id)

    def __str__(self):
        return self.name

    def __init__(self):
        return

    def sign_in(self, use_credentials):
        return

    def find_books(self, books, search_author, search_title, keyword_search):
        return False

    def get_book_info(self, book_id, cache):
        return None

    def get_current_book_availability(self, book_id):
        self.log.info('Get current availability not supported by %s' % self.name)
        return None

    def calculate_wait_weeks(self, library_copies=0, available_copies=0, have_checked_out=False,
                             hold_position_overall=None, hold_position_per_copy=None,
                             number_waiting_overall=None, number_waiting_per_copy=None,
                             estimated_wait_days=None, release_date=None, availability_date=None,
                             avg_loan_weeks=AVG_LOAN_WEEKS):

        # estimate wait (in weeks) until book can be check_availability_actioned
        # library_copies and/or available_copies may be set True for unlimited

        if release_date is not None:
            wait_until_release = max((release_date.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() / SECONDS_PER_WEEK, 0)
        else:
            wait_until_release = NO_WAIT_WEEKS

        #print('wait until release = %0.1f weeks' % wait_until_release)

        if have_checked_out:
            wait = NO_WAIT_WEEKS        # already have it

        elif estimated_wait_days is not None:
            wait = estimated_wait_days / 7.0    # library-supplied estimate

        elif availability_date is not None:
            # library-supplied
            wait = max((availability_date.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() / SECONDS_PER_WEEK, 0)

        elif available_copies or (library_copies is True):
            wait = wait_until_release   # available now (or at release)

        elif not library_copies:
            wait = None                 # never available?

        else:
            # estimate wait based on number of people ahead of the user

            if hold_position_per_copy is not None:
                waiting_ahead_per_copy = hold_position_per_copy - 1
            elif hold_position_overall is not None:
                waiting_ahead_per_copy = (hold_position_overall - 1) / library_copies
            elif number_waiting_per_copy is not None:
                waiting_ahead_per_copy = number_waiting_per_copy
            elif number_waiting_overall is not None:
                waiting_ahead_per_copy = number_waiting_overall / library_copies
            else:
                waiting_ahead_per_copy = 0   # not enough information to determine!

            if wait_until_release == NO_WAIT_WEEKS:
                # already released so currently on loan. assume half done on average.
                readers_ahead_per_copy = waiting_ahead_per_copy + 0.5
            else:
                readers_ahead_per_copy = waiting_ahead_per_copy

            #print('number readers ahead of user per copy = %0.1f' % readers_ahead_per_copy)

            avg_loan_weeks = tweaks.get(TWEAK_AVG_LOAN_WEEKS, avg_loan_weeks)
            wait = wait_until_release + (readers_ahead_per_copy * avg_loan_weeks)
            #print('calculated wait = %0.1f' % wait)

        # log only significant information
        log_args = []
        if library_copies:
            log_args.append('library_copies=%s' % library_copies)
        if available_copies:
            log_args.append('available_copies=%s' % available_copies)
        if hold_position_per_copy:
            log_args.append('hold_position_per_copy=%s' % hold_position_per_copy)
        if hold_position_overall:
            log_args.append('hold_position_overall=%s' % hold_position_overall)
        if number_waiting_per_copy:
            log_args.append('number_waiting_per_copy=%s' % number_waiting_per_copy)
        if number_waiting_overall:
            log_args.append('number_waiting_overall=%s' % number_waiting_overall)
        if have_checked_out:
            log_args.append('have_checked_out=%s' % have_checked_out)
        if estimated_wait_days:
            log_args.append('estimated_wait_days=%s' % estimated_wait_days)
        if release_date:
            log_args.append('release_date=%s' % (str(release_date)[:10]))
        if availability_date:
            log_args.append('availability_date=%s' % (str(availability_date)[:10]))

        if wait is None:
            self.log.info('Calculated unknown wait at %s: %s' % (self.name, ' '.join(log_args)))
        else:
            wait = min(max(int(math.ceil(wait)), NO_WAIT_WEEKS), MAX_WAIT_WEEKS)  # force to integer within limits
            self.log.info('Calculated %d week wait at %s: %s' % (wait, self.name, ' '.join(log_args)))

        return wait


def full_library_id(provider_id, library_id):
    if not provider_id:
        return library_id

    return '%s/%s' % (provider_id, library_id)
