import ast
import copy
import json
import os
import re

from django.utils.translation import gettext_lazy as _
from django.utils.functional import Promise

from orchestra.utils.paths import get_project_dir

from . import Setting


class Remove(object):
    """ used to signal a setting remove """
    pass


def get_settings_file():
    return os.path.join(get_project_dir(), 'settings.py')


def _find_updates(changes, settings_file):
    """ find all updates needed for applying changes on settings_file content """
    with open(settings_file, 'rb') as handler:
        p = ast.parse(handler.read())
    updates = []
    for elem in p.body:
        if updates and updates[-1][-1] is None:
            updates[-1][-1] = elem.lineno-1
        targets = getattr(elem, 'targets', None)
        if targets:
            var_name = targets[0].id
            if var_name in changes:
                updates.append([var_name, elem.lineno, None])
    return updates


class LazyUgettextRepr(object):
    def __init__(self, value):
        self.value = value
    
    def __repr__(self):
        return '_("%s")' % self.value
    
    def __len__(self):
        return len(repr(self.value))


class NotSupported(object):
    def __repr__(self):
        return 'Serialization not supported'
    
    def __len__(self):
        return 0


def get_eval_context():
    return {
        'NotSupported': NotSupported,
        '_': _,
    }


def serialize(obj, init=True):
    if isinstance(obj, NotSupported):
        return obj
    elif isinstance(obj, Promise):
        _obj = LazyUgettextRepr(obj)
    elif isinstance(obj, dict):
        _obj = {}
        for name, value in obj.items():
            name = serialize(name, init=False)
            value = serialize(value, init=False)
            if isinstance(name, NotSupported) or isinstance(value, NotSupported):
                return NotSupported()
            _obj[name] = value
    elif isinstance(obj, (tuple, list)):
        _obj = []
        for nested in obj:
            nested = serialize(nested, init=False)
            if isinstance(nested, NotSupported):
                return nested
            _obj.append(nested)
        _obj = type(obj)(_obj)
    elif isinstance(obj, (str, bool, int, float)):
        _obj = obj
    else:
        _obj = NotSupported()
    return repr(_obj) if init else _obj


def _format_setting(name, value):
    if isinstance(value, Remove):
        return ""
    try:
        value = json.dumps(value, indent=4)
    except TypeError:
        value = serialize(value)
    return "{name} = {value}".format(name=name, value=value)


def validate_changes(changes):
    for name, value in changes.items():
        if not isinstance(value, Remove):
            try:
                setting = Setting.settings[name]
            except KeyError:
                pass
            else:
                setting.validate_value(value)


def apply(changes, settings_file=get_settings_file()):
    """ returns settings_file content with applied changes """
    validate_changes(changes)
    updates = _find_updates(changes, settings_file)
    content = []
    _changes = copy.copy(changes)
    inside = False
    lineno = None
    if updates:
        name, lineno, end = updates.pop(0)
    # update existing variable declarations
    with open(settings_file, 'r') as handler:
        for num, line in enumerate(handler.readlines(), 1):
            line = line.rstrip()
            if num == lineno:
                value = _changes.pop(name)
                line = _format_setting(name, value)
                if line:
                    content.append(line)
                inside = True
                comments = []
                lastend = end
                try:
                    name, lineno, end = updates.pop(0)
                except IndexError:
                    if lastend is None:
                        break
            if not inside:
                content.append(line)
            else:
                # Discard lines since variable will be replaced
                # But save comments and blank lines
                if re.match(r'^\s*(#.*)*\s*$', line):
                    comments.append(line)
                else:
                    comments = []
                # End of variable declaration
                if num == lastend:
                    content.extend(comments)
                    inside = False
    
    # insert new variables at the end of file
    for name, value in _changes.items():
        content.append(_format_setting(name, value))
    return '\n'.join(content)


def save(changes, settings_file=get_settings_file(), backup=True):
    """ apply changes to project.settings file, saving a backup """
    new_settings = apply(changes, settings_file)
    tmp_settings_file = settings_file + '.tmp'
    with open(tmp_settings_file, 'w') as handle:
        handle.write(new_settings)
    if backup:
        os.rename(settings_file, settings_file + '.backup')
    os.rename(tmp_settings_file, settings_file)