#!/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 io
import os
import re


from calibre.constants import (config_dir, numeric_version)
from calibre.utils.config_base import tweaks
from calibre.utils.logging import (Log, HTMLStream, INFO)

from calibre_plugins.overdrive_link.tweak import TWEAK_SAVE_RESPONSES_ON_ERROR

from .python_transition import (IS_PYTHON2)
if IS_PYTHON2:
    from .python_transition import (chr, html, repr)
else:
    import html
    import html.parser


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


PLUGIN_DIR = 'plugins'
LOG_DIR = 'Overdrive Link Logs'


class ODStatus(object):
    '''
    Handle job status notification to a queue.
    Allows status for substeps in multi-layered sets of steps.
    '''
    def __init__(self, queue, prefix='', start=0.0, range=1.0):
        self.queue = queue              # notification queue
        self.prefix = prefix
        self.start = start
        self.range = range

    def subrange(self, frac_start, frac_end, msg):
        new_status = ODStatus(
            queue=self.queue,
            prefix="-".join([s for s in [self.prefix, msg] if s]),
            start=self.start + (frac_start * self.range),
            range=(frac_end - frac_start) * self.range)

        new_status.update(0, '')
        return new_status

    def update_subrange(self, num, total, msg):
        return self.subrange(num/total, (num+1)/total, msg)

    def update(self, frac_complete, msg):
        self.queue.put((max(self.start + (frac_complete * self.range), 0.01), "-".join([s for s in [self.prefix, msg] if s])))


class ODHTMLStream(HTMLStream):
    '''
    Logging stream that produces a cleaner job details display than HTMLStream does for ParallelJob
    '''

    def prints(self, level, *args, **kwargs):
        if numeric_version < (5, 7, 0):
            kwargs['file'] = self.stream

        if level == INFO:
            self._prints(*args, **kwargs)   # Don't add <span> tags for normal INFO logs
        else:
            self.stream.write(self.color[level])
            new_args = list(args)
            new_args.append(self.normal)    # Merge </span> with preceding text for cleaner output
            self._prints(*new_args, **kwargs)


class ODLog(Log):
    def __init__(self):
        Log.__init__(self, level=Log.DEBUG)
        self.outputs = [ODHTMLStream()]   # output to sys.stdout with html formatting


class JobLog(object):
    '''
    Logger that also collects errors and warnings for presentation in a job summary.
    '''

    ext_of_type = {
            'application/javascript': '.js',
            'application/json': '.json',
            'application/octet-stream': '.bin',
            'application/x-bzip2': '.bz2',
            'application/x-font-ttf': '.ttf',
            'application/x-javascript': '.js',
            'application/x-unknown': '.bin',
            'image/gif': '.gif',
            'image/jpeg': '.jpg',
            'image/png': '.png',
            'image/svg+xml': '.svg',
            'text/css': '.css',
            'text/html': '.htm',
            'text/javascript': '.js',
            'text/plain': '.txt',
            'x-unknown': '.bin',
            }

    def __init__(self, logger, errors, warnings, summaries):
        self.logger = logger
        self.errors = errors
        self.warnings = warnings
        self.summaries = summaries

        self.error_context = None

        # for logging responses from web sites
        self.response_seq = 0
        self.response_filename = None
        self.response_data = None
        self.response_type = None

    def debug(self, msg):
        if self.logger is not None:
            self.logger.debug(html_escape(msg))

    def info(self, msg):
        if self.logger is not None:
            self.logger.info(html_escape(msg))

    def warn(self, desc):
        msg = self.add_context(desc)
        self.warnings.append(msg)
        if self.logger is not None:
            self.logger.warn(html_escape(msg))

    def error(self, desc):
        msg = self.add_context(desc)
        self.errors.append(msg)
        if self.logger is not None:
            self.logger.error(html_escape(msg))
        self.response()

    def exception(self, desc, e):
        msg = self.add_context('%s: %s' % (desc, repr(e)))
        self.errors.append(msg)
        if self.logger is not None:
            self.logger.exception(html_escape(msg))
        self.response()

    def summary(self, msg):
        self.summaries.append(msg)
        self.info(msg)

    def has_errors(self):
        return len(self.errors) > 0

    def context(self, desc):
        # set context for subsequent errors and warnings
        self.error_context = desc
        self.clear_response()

        #if desc:
        #    self.info(desc) # detailed log

    def add_context(self, msg):
        if not self.error_context:
            return msg

        return '%s: %s' % (self.error_context, msg)

    def save_response(self, name, data, response_type):
        # save response for logging in case of error

        if tweaks.get(TWEAK_SAVE_RESPONSES_ON_ERROR, False):
            self.response_filename = ('response-%06d-%s' % (
                self.response_seq, re.sub(r'[^A-Za-z0-9._-]', '', name.replace(' ', '-'))[:80]))
            ext = self.ext_of_type.get(response_type, '.htm')

            if not self.response_filename.endswith(ext):
                self.response_filename += ext

            self.response_data = data
            self.response_type = response_type
        else:
            self.clear_response()

        self.response_seq += 1

    def response(self):
        # log last saved response to a file

        if self.response_filename:
            log_dir = os.path.join(config_dir, PLUGIN_DIR, LOG_DIR)

            if not os.path.exists(log_dir):
                try:
                    os.mkdir(log_dir)
                except Exception:
                    pass

            filename = os.path.join(log_dir, self.response_filename)

            try:
                with io.open(filename, 'wb') as of:
                    of.write(self.response_data if isinstance(self.response_data, bytes) else
                             self.response_data.encode('utf8', errors='replace'))

                self.info('Response %s saved to: %s' % (self.response_type, filename))

            except Exception as e:
                self.clear_response()
                self.exception('Saving response to file %s' % filename, e)

            self.clear_response()

    def clear_response(self):
        self.response_filename = self.response_data = self.response_type = None


class HTMLTextExtractor(html.parser.HTMLParser):
    def __init__(self):
        html.parser.HTMLParser.__init__(self)
        self.result = []

    def handle_data(self, d):
        self.result.append(d)

    def handle_charref(self, number):
        codepoint = int(number[1:], 16) if number[0] in (u'x', u'X') else int(number)
        self.result.append(chr(codepoint))

    def handle_entityref(self, name):
        if name in html.entities.name2codepoint:
            codepoint = html.entities.name2codepoint[name]
            self.result.append(chr(codepoint))
        else:
            self.result.append('&%s;' % name)   # cannot decode, leave entity unchanged

    def get_text(self):
        return u''.join(self.result)


def html_to_text(htm):
    s = HTMLTextExtractor()
    s.feed(htm)
    return s.get_text()


def html_escape(msg):
    return html.escape(msg, quote=False)
