import logging

import pytz
import re
from PIL import Image
from django import forms
from django.core.files.images import get_image_dimensions
from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
from django.forms.utils import ErrorList
from django.utils.translation import ugettext_lazy as _, ungettext_lazy

import boards.settings as board_settings
from boards import utils
from boards.abstracts.constants import REGEX_TAGS
from boards.abstracts.settingsmanager import get_settings_manager, \
    SETTING_CONFIRMED_USER
from boards.abstracts.sticker_factory import get_attachment_by_alias
from boards.forms.fields import UrlFileField
from boards.forms.validators import TimeValidator, PowValidator
from boards.mdx_neboard import formatters
from boards.models import Attachment
from boards.models import Tag
from boards.models.attachment import StickerPack
from boards.models.attachment.downloaders import download, REGEX_MAGNET
from boards.models.attachment.viewers import FILE_TYPES_IMAGE
from boards.models.post import TITLE_MAX_LENGTH
from boards.settings import SECTION_FORMS
from boards.utils import validate_file_size, get_file_mimetype, \
    FILE_EXTENSION_DELIMITER, get_tripcode_from_text

FORMAT_PANEL_BUTTON = '<span class="mark_btn" ' \
                      'onClick="addMarkToMsg(\'{}\',  \'{}\')">{}{}{}</span>'


REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)

VETERAN_POSTING_DELAY = 5

ATTRIBUTE_PLACEHOLDER = 'placeholder'
ATTRIBUTE_ROWS = 'rows'

TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
TAGS_PLACEHOLDER = _('music images i_dont_like_tags')

LABEL_TITLE = _('Title')
LABEL_TEXT = _('Text')
LABEL_TAG = _('Tag')
LABEL_SEARCH = _('Search')
LABEL_FILE = _('File')
LABEL_DUPLICATES = _('Check for duplicates')
LABEL_URL = _('Do not download URLs')

ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
ERROR_DUPLICATES = 'Some files are already present on the board.'

TAG_MAX_LENGTH = 20

TEXTAREA_ROWS = 4

TRIPCODE_DELIM = '##'

# TODO Maybe this may be converted into the database table?
MIMETYPE_EXTENSIONS = {
    'image/jpeg': 'jpeg',
    'image/png': 'png',
    'image/gif': 'gif',
    'video/webm': 'webm',
    'application/pdf': 'pdf',
    'x-diff': 'diff',
    'image/svg+xml': 'svg',
    'application/x-shockwave-flash': 'swf',
    'image/x-ms-bmp': 'bmp',
    'image/bmp': 'bmp',
}

DOWN_MODE_DOWNLOAD = 'DOWNLOAD'
DOWN_MODE_DOWNLOAD_UNIQUE = 'DOWNLOAD_UNIQUE'
DOWN_MODE_URL = 'URL'
DOWN_MODE_TRY = 'TRY'


logger = logging.getLogger('boards.forms')


def get_timezones():
    timezones = []
    for tz in pytz.common_timezones:
        timezones.append((tz, tz),)
    return timezones


class FormatPanel(forms.Textarea):
    """
    Panel for text formatting. Consists of buttons to add different tags to the
    form text area.
    """

    def render(self, name, value, attrs=None):
        output_template = '<div id="mark-panel">{}</div>'

        buttons = [self._get_button(formatter) for formatter in formatters]

        output = output_template.format(''.join(buttons))

        output += super(FormatPanel, self).render(name, value, attrs=attrs)

        return output

    def _get_button(self, formatter):
        return FORMAT_PANEL_BUTTON.format(
            formatter.format_left,
            formatter.format_right,
            formatter.preview_left,
            formatter.name,
            formatter.preview_right)


class PlainErrorList(ErrorList):
    def __unicode__(self):
        return self.as_text()

    def as_text(self):
        return ''.join(['(!) %s ' % e for e in self])


class NeboardForm(forms.Form):
    """
    Form with neboard-specific formatting.
    """
    required_css_class = 'required-field'

    def as_div(self):
        """
        Returns this form rendered as HTML <as_div>s.
        """

        return self._html_output(
            # TODO Do not show hidden rows in the list here
            normal_row='<div class="form-row">'
                       '<div class="form-label">'
                       '%(label)s'
                       '</div>'
                       '<div class="form-input">'
                       '%(field)s'
                       '</div>'
                       '</div>'
                       '<div class="form-row">'
                       '%(help_text)s'
                       '</div>',
            error_row='<div class="form-row">'
                      '<div class="form-label"></div>'
                      '<div class="form-errors">%s</div>'
                      '</div>',
            row_ender='</div>',
            help_text_html='%s',
            errors_on_separate_row=True)

    def as_json_errors(self):
        errors = []

        for name, field in list(self.fields.items()):
            if self[name].errors:
                errors.append({
                    'field': name,
                    'errors': self[name].errors.as_text(),
                })

        return errors


class PostForm(NeboardForm):

    title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
                            label=LABEL_TITLE,
                            widget=forms.TextInput(
                                attrs={ATTRIBUTE_PLACEHOLDER: 'Title{}tripcode'.format(TRIPCODE_DELIM)}))
    text = forms.CharField(
        widget=FormatPanel(attrs={
            ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
            ATTRIBUTE_ROWS: TEXTAREA_ROWS,
        }),
        required=False, label=LABEL_TEXT)
    download_mode = forms.ChoiceField(
            choices=(
                (DOWN_MODE_TRY, _('Download or insert as URLs')),
                (DOWN_MODE_DOWNLOAD, _('Download')),
                (DOWN_MODE_DOWNLOAD_UNIQUE, _('Download and check for uniqueness')),
                (DOWN_MODE_URL, _('Insert as URLs')),
            ),
            initial=DOWN_MODE_TRY,
            label=_('File process mode'))
    file = UrlFileField(required=False, label=LABEL_FILE)

    # This field is for spam prevention only
    email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
                            widget=forms.TextInput(attrs={
                                'class': 'form-email'}))
    subscribe = forms.BooleanField(required=False,
                                   label=_('Subscribe to thread'))

    guess = forms.CharField(widget=forms.HiddenInput(), required=False)
    timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
    iteration = forms.CharField(widget=forms.HiddenInput(), required=False)

    need_to_ban = False

    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
                 initial=None, error_class=ErrorList, label_suffix=None,
                 empty_permitted=False, field_order=None,
                 use_required_attribute=None, renderer=None, session=None):
        super().__init__(data, files, auto_id, prefix, initial, error_class,
                         label_suffix, empty_permitted, field_order,
                         use_required_attribute, renderer)

        self.session = session

    def clean_title(self):
        title = self.cleaned_data['title']
        if title:
            if len(title) > TITLE_MAX_LENGTH:
                raise forms.ValidationError(_('Title must have less than %s '
                                              'characters') %
                                            str(TITLE_MAX_LENGTH))
        return title

    def clean_text(self):
        text = self.cleaned_data['text'].strip()
        if text:
            max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
            if len(text) > max_length:
                raise forms.ValidationError(_('Text must have less than %s '
                                              'characters') % str(max_length))
        return text

    def clean_file(self):
        return self._clean_files(self.cleaned_data['file'])

    def clean(self):
        cleaned_data = super(PostForm, self).clean()

        if cleaned_data['email']:
            if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
                self.need_to_ban = True
            raise forms.ValidationError('A human cannot enter a hidden field')

        if not self.errors:
            self._clean_text_file()

        limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
        limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')

        settings_manager = get_settings_manager(self)
        if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting(SETTING_CONFIRMED_USER)):
            pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
            if pow_difficulty > 0:
                validator = PowValidator(
                    self.session, cleaned_data['timestamp'],
                    cleaned_data['iteration'], cleaned_data['guess'],
                    cleaned_data['text'])
            else:
                validator = TimeValidator(self.session)

            validator.validate()
            for error in validator.get_errors():
                self._add_general_error(error)

            settings_manager.set_setting(SETTING_CONFIRMED_USER, True)
        if self.cleaned_data['download_mode'] == DOWN_MODE_DOWNLOAD_UNIQUE:
            self._check_file_duplicates(self.get_files())

        return cleaned_data

    def get_files(self):
        """
        Gets file from form or URL.
        """

        files = []
        for file in self.cleaned_data['file']:
            if isinstance(file, UploadedFile):
                files.append(file)

        return files

    def get_file_urls(self):
        files = []
        for file in self.cleaned_data['file']:
            if type(file) == str:
                files.append(file)

        return files

    def get_tripcode(self):
        title = self.cleaned_data['title']
        if title is not None and TRIPCODE_DELIM in title:
            tripcode = get_tripcode_from_text(title.split(TRIPCODE_DELIM, maxsplit=1)[1])
        else:
            tripcode = ''
        return tripcode

    def get_title(self):
        title = self.cleaned_data['title']
        if title is not None and TRIPCODE_DELIM in title:
            return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
        else:
            return title

    def get_images(self):
        return self.cleaned_data.get('stickers', [])

    def is_subscribe(self):
        return self.cleaned_data['subscribe']

    def _update_file_extension(self, file):
        if file:
            mimetype = get_file_mimetype(file)
            extension = MIMETYPE_EXTENSIONS.get(mimetype)
            if extension:
                filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
                new_filename = filename + FILE_EXTENSION_DELIMITER + extension

                file.name = new_filename
            else:
                logger.info('Unrecognized file mimetype: {}'.format(mimetype))

    def _clean_files(self, inputs):
        files = []

        max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
        if len(inputs) > max_file_count:
            raise forms.ValidationError(
                ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
                               max_file_count) % {'files': max_file_count})

        size = 0
        for file_input in inputs:
            if isinstance(file_input, UploadedFile):
                file = self._clean_file_file(file_input)
                size += file.size
                files.append(file)
            else:
                files.append(self._clean_file_url(file_input))

        for file in files:
            self._validate_image_dimensions(file)
        validate_file_size(size)

        return files

    def _validate_image_dimensions(self, file):
        if isinstance(file, UploadedFile):
            mimetype = get_file_mimetype(file)
            if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
                Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
                try:
                    print(get_image_dimensions(file))
                except Exception:
                    raise forms.ValidationError('Possible decompression bomb or large image.')

    def _clean_file_file(self, file):
        self._update_file_extension(file)

        return file

    def _clean_file_url(self, url):
        file = None

        if url:
            mode = self.cleaned_data['download_mode']
            if mode == DOWN_MODE_URL:
                return url

            try:
                image = get_attachment_by_alias(url, self.session)
                if image is not None:
                    if 'stickers' not in self.cleaned_data:
                        self.cleaned_data['stickers'] = []
                    self.cleaned_data['stickers'].append(image)
                    return

                if file is None:
                    file = self._get_file_from_url(url)
                    if not file:
                        raise forms.ValidationError(_('Invalid URL'))
                self._update_file_extension(file)
            except forms.ValidationError as e:
                # Assume we will get the plain URL instead of a file and save it
                if mode == DOWN_MODE_TRY and (REGEX_URL.match(url) or REGEX_MAGNET.match(url)):
                    logger.info('Error in forms: {}'.format(e))
                    return url
                else:
                    raise e

        return file

    def _clean_text_file(self):
        text = self.cleaned_data.get('text')
        file = self.get_files()
        file_url = self.get_file_urls()
        images = self.get_images()

        if (not text) and (not file) and (not file_url) and len(images) == 0:
            error_message = _('Either text or file must be entered.')
            self._add_general_error(error_message)

    def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
        """
        Gets an file file from URL.
        """

        try:
            return download(url)
        except forms.ValidationError as e:
            raise e
        except Exception as e:
            raise forms.ValidationError(e)

    def _check_file_duplicates(self, files):
        for file in files:
            file_hash = utils.get_file_hash(file)
            if Attachment.objects.get_existing_duplicate(file_hash, file):
                self._add_general_error(_(ERROR_DUPLICATES))

    def _add_general_error(self, message):
        self.add_error('text', forms.ValidationError(message))


class ThreadForm(PostForm):

    tags = forms.CharField(
        widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
        max_length=100, label=_('Tags'), required=True)
    monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
    stickerpack = forms.BooleanField(label=_('Sticker Pack'), required=False)

    def clean_tags(self):
        tags = self.cleaned_data['tags'].strip()

        if not tags or not REGEX_TAGS.match(tags):
            raise forms.ValidationError(
                _('Inappropriate characters in tags.'))

        default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
                .strip().lower()

        required_tag_exists = False
        tag_set = set()
        for tag_string in tags.split():
            tag_name = tag_string.strip().lower()
            if tag_name == default_tag_name:
                required_tag_exists = True
                tag, created = Tag.objects.get_or_create_with_alias(
                        name=tag_name, required=True)
            else:
                tag, created = Tag.objects.get_or_create_with_alias(name=tag_name)
            tag_set.add(tag)

            # If this is a new tag, don't check for its parents because nobody
            # added them yet
            if not created:
                tag_set |= set(tag.get_all_parents())

        for tag in tag_set:
            if tag.required:
                required_tag_exists = True
                break

        # Use default tag if no section exists
        if not required_tag_exists:
            default_tag, created = Tag.objects.get_or_create_with_alias(
                    name=default_tag_name, required=True)
            tag_set.add(default_tag)

        return tag_set

    def clean(self):
        cleaned_data = super(ThreadForm, self).clean()

        return cleaned_data

    def is_monochrome(self):
        return self.cleaned_data['monochrome']

    def clean_stickerpack(self):
        stickerpack = self.cleaned_data['stickerpack']
        if stickerpack:
            tripcode = self.get_tripcode()
            if not tripcode:
                raise forms.ValidationError(_(
                    'Tripcode should be specified to own a stickerpack.'))
            title = self.get_title()
            if not title:
                raise forms.ValidationError(_(
                    'Title should be specified as a stickerpack name.'))
            if not REGEX_TAGS.match(title):
                raise forms.ValidationError(_('Inappropriate sticker pack name.'))

            existing_pack = StickerPack.objects.filter(name=title).first()
            if existing_pack:
                if existing_pack.tripcode != tripcode:
                    raise forms.ValidationError(_(
                        'A sticker pack with this name already exists and is'
                        ' owned by another tripcode.'))
                if not existing_pack.tripcode:
                    raise forms.ValidationError(_(
                        'This sticker pack can only be updated by an '
                        'administrator.'))

        return stickerpack

    def is_stickerpack(self):
        return self.cleaned_data['stickerpack']


class SettingsForm(NeboardForm):

    theme = forms.ChoiceField(
            choices=board_settings.get_list_dict('View', 'Themes'),
            label=_('Theme'))
    image_viewer = forms.ChoiceField(
            choices=board_settings.get_list_dict('View', 'ImageViewers'),
            label=_('Image view mode'))
    username = forms.CharField(label=_('User name'), required=False)
    timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
    subscribe_by_default = forms.BooleanField(
        required=False, label=_('Subscribe to threads by default'))

    def clean_username(self):
        username = self.cleaned_data['username']

        if username and not REGEX_USERNAMES.match(username):
            raise forms.ValidationError(_('Inappropriate characters.'))

        return username


class SearchForm(NeboardForm):
    query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)