# -*- coding: utf-8 -*-

"""
Form field definitions.

Questions: Why everything be abstract? I knew this once upon a time, but alas I forgat.
"""

import time # needed for ordered attributes
import datetime
import inspect
import typing
import flask

# parent imports
import poobrains
from poobrains import app
import poobrains.helpers
import poobrains.errors
import poobrains.rendering

# internal imports
from . import types


@app.template_test(name="optgroup")
def is_optgroup(value):
    return isinstance(value, tuple) or isinstance(value, list)


class BoundFieldMeta(poobrains.helpers.MetaCompatibility, poobrains.helpers.ClassOrInstanceBound):
    pass


class BaseField(object, metaclass=poobrains.helpers.MetaCompatibility):

    _created = None
    _default = None
    _choices = None
    value = None
    form = None # only filled if this is a field rendered outside of the form
    errors = None
    prefix = None
    name = None
    type = types.STRING
    value = None
    choices = None
    default = None # used when client sends no value for this field
    label = None
    multi = False # Whether this field takes multiple values (i.e. value passed to .bind will be a list)
    placeholder = None
    help_text = None
    readonly = None
    required = None
    validators = None


    class Meta:
        clone_props = ['name', 'type', 'multi', 'value', 'choices', 'label', 'placeholder', 'help_text', 'readonly', 'required', 'validators', 'default']


    def __new__(cls, *args, **kwargs):

        instance = super(BaseField, cls).__new__(cls)
        instance._created = time.time()

        return instance


    def __init__(self, name=None, type=None, multi=None, value=None, choices=None, label=None, placeholder=None, help_text=None, readonly=False, required=False, validators=None, default=None, form=None, **kwargs):

        """
        Parameters:
            form: optional; A form object. To be passed when creating a Field outside of a form, used to add it into the forms bind/validate/process mechanism
        """

        self.errors = []
        self.name = name
        self.label = label
        self.placeholder = placeholder
        self._help_text = help_text
        self.readonly = readonly
        self.required = required
        self.rendered = False

        if not type is None:
            self.type = type

        if not multi is None:
            self.multi = multi

        if not choices is None:
            self.choices = choices # leave choices of other __init__ intact. <- TODO: what other __init__? did i mean statically defined properties in subclasses?

        if not default is None:
            self.default = default

        if self.default is None and self.multi:
            self.default = []

        if not value is None:
            self.value = value
        else:
            self.value = self.default

        if not validators is None:
            self.validators = validators
        else:
            self.validators = []

        if not form is None: # means we're dealing with an external field

            self.form = form
            if self.form.prefix:
                self.prefix = "%s.%s" % (self.form.prefix, self.form.name)
            else:
                self.prefix = self.form.name
            self.form.add_field(self, external=True) # NOTE: this has to be called *after* self.choices is filled


    def __setattr__(self, name, value):

        if name == 'name' and isinstance(value, str):
            assert not '.' in value, "Form Field names *must* not contain dots: %s" % value

        super(BaseField, self).__setattr__(name, value)


    def templates(self, mode=None):

        tpls = []

        for x in [self.__class__] + self.__class__.ancestors():

            name = x.__name__.lower()

            if issubclass(x, Field):

                tpls.append('form/fields/%s.jinja' % name)
                
                if mode:
                    tpls.append('form/fields/%s-%s.jinja' % (name, mode))

            else:

                tpls.append('%s.jinja' % name)

                if mode:
                    tpls.append('%s-%s.jinja' % (name, mode))

        return tpls

    @property
    def help_text(self):
        if isinstance(self._help_text, str) and len(self._help_text):
            if '\n' in self._help_text: # unindent multi-line help texts so they're not seen as code blocks by markdown
                lines = self._help_text.split('\n')
                indent = len(lines[1]) - len(lines[1].lstrip(' '))
                unindented_lines = []
                for line in lines:
                    unindented_lines.append(line[indent:])

                return poobrains.md.MarkdownString('\n'.join(unindented_lines), css_class='multiline')
            return poobrains.md.MarkdownString(self._help_text)
        return self._help_text
    @help_text.setter
    def help_text(self, value):
        self._help_text = value

    @property
    def default(self):
        if callable(self._default):
            return self._default()
        return self._default
    @default.setter
    def default(self, value):
        self._default = value

    @property
    def choices(self):
        if callable(self._choices):
            return self._choices()
        return self._choices
    @choices.setter
    def choices(self, value):
        self._choices = value

    @property
    def ref_id(self):

        """ HTML 'id' attribute value"""
        if self.prefix:
            return '-'.join((poobrains.helpers.clean_string(self.prefix), poobrains.helpers.clean_string(self.name)))

        return self.name


    @property
    def list_id(self):

        """ HTML 'id' attribute for a datalist to be associated with this form element. """
        if self.choices:
            return "list-%s" % self.ref_id

        return False # no choices means no datalist

    
    @property
    def valid_choices(self):

        r = []
        for value, _ in self.choices:
            if is_optgroup(value): 
                for value, _ in value:
                    r.append(value)
            else:
                r.append(value)

        return r



    @property
    def empty(self):

        return (self.multi and self.value == []) or self.value is None

    
    def checked(self, value):

        if self.multi:
            return value in self.value

        return value == self.value


    def validate(self):

        compound_error = poobrains.errors.CompoundError()

        values = self.value if self.multi else [self.value]

        if not self.empty:

            for value in values:

                if self.choices and not value in self.valid_choices:
                    compound_error.append(poobrains.errors.ValidationError("'%s' is not an approved choice for %s.%s" % (self.value, self.prefix, self.name)))

                else: # else because we don't want to clutter error output 
                    for validator in self.validators:
                        try:
                            validator(self.value)
                        except poobrains.errors.ValidationError as e:
                            compound_error.append(e)

        elif self.required:
            compound_error.append(poobrains.errors.ValidationError("Required field '%s' was left empty." % self.name))

        if len(compound_error):
            raise compound_error


    def bind(self, value):

        compound_error = poobrains.errors.CompoundError()

        empty = value == '' or (self.multi and value == []) # TODO: value in [[], ['']] if <input type="text" multiple> needs it!

        if empty:

            self.value = self.default

        else:

            if self.multi:

                self.value = [] # clear any already existing values
                for subvalue in value:

                    try:
                        self.value.append(self.type.convert(subvalue, None, None))

                    except poobrains.errors.BadParameter:

                        e = poobrains.errors.ValidationError("Invalid input '%s' for field %s.%s." % (value, self.prefix, self.name))
                        compound_error.append(e)
                        self.errors.append(e)

            else:

                try:
                    self.value = self.type.convert(value, None, None)

                except poobrains.errors.BadParameter:

                    e = poobrains.errors.ValidationError("Invalid input '%s' for field %s.%s." % (value, self.prefix, self.name))
                    compound_error.append(e)
                    self.errors.append(e)

        try:
            self.validate()

        except poobrains.errors.CompoundError as ce:

            for e in ce:
                compound_error.append(e)
                self.errors.append(e)

        if len(compound_error):

            raise compound_error


    def value_string(self, value):

        """ Create a string representation of a (potential) field value for use in HTML """

        if value is None:
            return ''

        elif isinstance(value, bool):
            return 'true' if value == True else 'false'

        elif isinstance(value, datetime.datetime):
            return value.strftime('%Y-%m-%d %H:%M:%S')

        elif inspect.isclass(value):
            return value.__name__

        elif isinstance(value, poobrains.rendering.Renderable) and hasattr(value, 'handle_string'): # hacky check whether this is a Storable. Can't use isinstance because we can't depend on storage here (since storage depends on form )
            return value.handle_string

        else:
            return str(value)


class Value(BaseField):
    """ To put a static value into the form. """

    def validate(self):
        pass


    def bind(self, value):
        pass


class Field(BaseField, poobrains.rendering.Renderable):

    class Meta:
        abstract = True

    rendered = None

    def render(self):

        self.rendered = True
        return super(Field, self).render()


class Text(Field):

    class Meta:
        abstract = True


class Number(Field):

    class Meta:
        abstract = True

    min = None
    max = None
    type = types.FLOAT

    def __init__(self, min=None, max=None, step='any', **kwargs):

        super().__init__(**kwargs)

        self.min = min
        self.max = max
        self.step = step

    def validate(self):

        super().validate()

        if not self.empty:

            if (self.min != None and self.value < self.min) or (self.max != None and self.value > self.max):
                raise poobrains.errors.ValidationError("%s: %d is out of range. Must be in range from %d to %d." % (self.name, self.value, self.min, self.max))


class Color(Field):

    class Meta:
        abstract = True


class Message(Field):

    class Meta:
        abstract = True

    def __repr__(self):
        return "<%s: %s>" % (self.__class__.__name__, self.value)
    
    def validate(self):
        pass

    def bind(self, value):
        pass


class RenderableWrapper(Field):

    def __init__(self, renderable, **kwargs):

        """
        This __init__ just bends around the args a bit so the `Renderable`
        this field wraps can be passed in as the first (and only) arg,
        instead of having to manually specify the `value` kwarg.

        This might be a bad idea concerning consistency, but OTOH
        we think all *Wrapper classes should take the thing they wrap
        as first arg.
        """

        kwargs['value'] = renderable
        super().__init__(**kwargs)

    class Meta:
        abstract = True

    @property
    def title(self):
        return self.value.title

    def validate(self):
        pass

    def bind(self, value):
        pass


class ObfuscatedText(Text):

    class Meta:
        abstract = True


class TextArea(Text):

    class Meta:
        abstract = True


class Range(Number):
    pass


class Date(Field):

    class Meta:
        abstract = True

    type = types.DATE


class DateTime(Field):

    class Meta:
        abstract = True

    type = types.DATETIME


class Select(Field):

    empty_label = 'Please choose'

    def __init__(self, empty_label=None, **kwargs):
        super().__init__(**kwargs)
        if not empty_label is None:
            self.empty_label = empty_label

    class Meta:
        clone_props = Field._meta.clone_props + ['empty_label']


class Checkbox(Field):

    class Meta:
        abstract = True

    type = types.BOOL
    default = False

    def __init__(self, **kwargs):

        if kwargs.get('multi', False):

            if not kwargs.get('choices', False):
                kwargs['choices'] = [] # Checkbox must have choices. None passed is valid, because external Checkboxes

            if not kwargs.get('default', False):
                kwargs['default'] = []

        super(Checkbox, self).__init__(**kwargs)


    @property
    def type_bool(self):
        return self.type == types.BOOL


    def checked(self, value):

        if self.type == types.BOOL:
            return value == True

        return super(Checkbox, self).checked(value)


    def value_string(self, value):

        if self.type == types.BOOL:
            return 'true'

        return super(Checkbox, self).value_string(value)


class Radio(Field):

    class Meta:
        abstract = True
    
    def __init__(self, *args, **kwargs):

        assert not kwargs.get('multi', False), "Radios buttons can't have multiple values by definition."
        assert kwargs.get('choices', False), "Radio buttons need choices passed."
        super(Radio, self).__init__(**kwargs)


class File(Field):

    class Meta:
        abstract = True
