# -*- coding: utf-8; -*-
#
# This file is part of Superdesk.
#
# Copyright 2013, 2017 Sourcefabric z.u. and contributors.
#
# For the full copyright and license information, please see the
# AUTHORS and LICENSE files distributed with this source code, or
# at https://www.sourcefabric.org/superdesk/license

import re
import regex
import logging
from lxml import etree  # noqa
from superdesk import etree as sd_etree
from lxml import html as lxml_html
from lxml.html import clean
from flask import current_app as app
import chardet
import html

logger = logging.getLogger(__name__)


# KEEP CHANGES IN SYNC WITH CLIENT FUNCTION `countWords`
# regex is used instead re to support unicode letter matching with \p{L}
def get_text_word_count(text):
    """Get word count for given plain text.

    :param str text: text string
    :return int: word count
    """

    flags = regex.MULTILINE | regex.UNICODE
    initial_text_trimmed = text.strip()

    if len(initial_text_trimmed) < 1:
        return 0

    r0 = get_text(initial_text_trimmed, space_on_elements=True)

    r1 = regex.sub(r"\n", " ", r0, flags=flags)

    # Remove spaces between two numbers
    # 1 000 000 000 -> 1000000000
    r2 = regex.sub(r"([0-9]) ([0-9])", "\\1\\2", r1, flags=flags)

    # remove anything that is not a unicode letter, a space or a number
    r3 = regex.sub(r"[^\p{L} 0-9]", "", r2, flags=flags)

    # replace two or more spaces with one space
    r4 = regex.sub(r" {2,}", " ", r3, flags=flags)

    result = len(r4.strip().split(" "))

    return result


def get_text(markup, content="xml", lf_on_block=False, space_on_elements=False, space=" "):
    """Get plain text version of (X)HTML or other XML element

    if the markup can't be parsed, it will be returned unchanged
    :param str markup: string to convert to plain text
    :param str content: 'xml' or 'html', as in parse_html
    :param bool lf_on_block: if True, add a line feed on block elements' tail
    :param bool space_on_elements: if True, add a space on each element's tail
        mainly used to count words with non HTML markup
    :param str space: space string which is used when `space_on_elements` is enabled
    :return str: plain text version of markup
    """
    try:
        root = sd_etree.parse_html(
            markup, content=content, lf_on_block=lf_on_block, space_on_elements=space_on_elements, space=space
        )
        text = etree.tostring(root, encoding="unicode", method="text")
        return text
    except etree.ParseError:
        return markup


def get_word_count(markup, no_html=False):
    """Get word count for given html.

    :param str markup: xhtml (or other xml) markup
    :param bool no_html: set to True if xml param is not (X)HTML
        if True, a space will be added after each element to separate words.
        This avoid to have construct like <hl2>word</hl2><p>another</p> (like in NITF)
        being counted as one word.
    :return int: count of words inside the text
    """
    if no_html:
        return get_text_word_count(get_text(markup, content="xml", space_on_elements=True))
    else:
        return get_text_word_count(get_text(markup, content="html", lf_on_block=True))


def update_word_count(update, original=None):
    """Update word count if there was change in content.

    :param update: created/updated document
    :param original: original document if updated
    """
    if update.get("body_html"):
        update.setdefault("word_count", get_word_count(update.get("body_html")))
    else:
        # If the body is removed then set the count to zero
        if original and "word_count" in original and "body_html" in update:
            update["word_count"] = 0


def get_char_count(html):
    """Get character count for given html.

    :param html: html string to count
    :return int: count of chars inside the text
    """
    return len(get_text(html))


def get_par_count(html):
    try:
        elem = sd_etree.parse_html(html, content="html")
        return len([p for p in elem.iterfind(".//p") if p.text and len(p.text.strip()) > 0])
    except ValueError as e:
        logger.warning(e)

    logger.warning("Failed to determine paragraph count from html: {}.".format(html))
    return 0


def get_reading_time(html, word_count=None, language=None):
    """Get estimanted number of minutes to read a text

    Check https://dev.sourcefabric.org/browse/SDFID-118 for details

    :param str html: html content
    :param int word_count: number of words in the text
    :param str language: language of the text
    :return int: estimated number of minute to read the text
    """
    if language and language.startswith("ja"):
        return round(len(re.sub(r"[\s]", "", get_text(html))) / app.config["JAPANESE_CHARACTERS_PER_MINUTE"])
    if not word_count:
        word_count = get_word_count(html)
    reading_time_float = word_count / 250
    reading_time_minutes = int(reading_time_float)
    reading_time_rem_sec = int((reading_time_float - reading_time_minutes) * 60)
    if reading_time_rem_sec >= 30:
        reading_time_minutes += 1
    return reading_time_minutes


def sanitize_html(html, remove_tags=None, kill_tags=None):
    """Sanitize HTML

    :param str html: unsafe HTML markup
    :return str: sanitized HTML
    """
    if not html:
        return ""

    if not kill_tags:
        kill_tags = ["script", "style", "head"]

    root_elem = lxml_html.fromstring(html)
    cleaner = clean.Cleaner(add_nofollow=False, kill_tags=kill_tags, remove_tags=remove_tags)
    cleaned_xhtml = cleaner.clean_html(root_elem)

    safe_html = etree.tostring(cleaned_xhtml, encoding="unicode")

    # the following code is legacy (pre-lxml)
    if safe_html == ", -":
        return ""

    return safe_html


def decode(bytes_str):
    """Decode bytes value

    try to decode using UTF-8, or to detect encoding. Will ignore bad chars as a last resort
    @return (str): decoded string
    """
    try:
        return bytes_str.decode("utf-8")
    except UnicodeDecodeError:
        try:
            return bytes_str.decode(chardet.detect(bytes_str)["encoding"])
        except Exception:
            return bytes_str.decode("utf-8", "ignore")


def plain_text_to_html(text: str) -> str:
    """Converts plain text to HTML

    This attempts to search for and convert:
    * Email address' to ``mailto`` HTML links
    * URL address' to HTML links
    * Newlines to a pair of ``<p>`` tags
    """

    EMAIL_REGEX = re.compile(r"([\w\-\.]+@(\w[\w\-]+\.)+[\w\-]+)", re.MULTILINE | re.UNICODE | re.IGNORECASE)
    URL_REGEX = re.compile(
        '((?:<a href[^>]+>)|(?:<a href="))?((https?://)?((?:[\w-]+\.)+[a-z]+(?:/\S*?)?)(?=[\.,)]?(?:\s|$)))',
        re.MULTILINE | re.UNICODE | re.IGNORECASE,
    )

    def replacement(match_object):
        groups = match_object.groups()
        if groups[0] is not None:
            # Since it has an anchor tag, this isn't what we want to change,
            # so return the whole match.
            return match_object.group(0)
        else:
            url = html.unescape(groups[1])
            if url.startswith("www."):
                url = "https://" + url
            elif not url.startswith(("http://", "https://")):
                url = "https://" + url
            return '<a href="{0}" target="_blank">{1}</a>'.format(url, groups[1])

    value = EMAIL_REGEX.sub(r'<a href="mailto:\1">\1</a>', text)
    value = re.sub(URL_REGEX, replacement, value)
    html_paragraphs = [line.strip() for line in value.splitlines()]
    return "<p>" + "</p><p>".join(html_paragraphs) + "</p>"
