import re

from .types import (convertValueToObjc, KeyValueId, ConstGenerator, NLSTR, Binding,
    generateDictionary)
from .property import Property
from . import globalvars

def upFirstLetter(s):
    return s[0].upper() + s[1:]

class CodeTemplate(object):
    def __init__(self, template):
        self._template = template
        self._replacements = {}
    
    def __getattr__(self, key):
        if key in self._replacements:
            return self._replacements[key]
        else:
            raise AttributeError()
    
    def __setattr__(self, key, value):
        if key in ['_template', '_replacements']:
            return object.__setattr__(self, key, value)
        self._replacements[key] = value
    
    def render(self):
        # Because we generate code and that code is likely to contain "{}" braces, it's better if we
        # use more explicit placeholders than the typecal format() method. These placeholders are
        # $name$.
        result = self._template
        replacements = self._replacements
        placeholders = re.findall(r"\$\w+?\$", result)
        while placeholders:
            # We run replacements multiple times because it's possible that one of our replacement
            # strings contain replacement placeholders. We want to perform replacements on those
            # strings too.
            for placeholder in placeholders:
                replacement = str(replacements.get(placeholder[1:-1], ''))
                result = result.replace(placeholder, replacement)
            placeholders = re.findall(r"\$\w+?\$", result)
        return result

owner = KeyValueId(None, 'owner')
NSApp = KeyValueId(None, 'NSApp')
const = ConstGenerator()
defaults = KeyValueId(None, 'NSUserDefaultsController').sharedUserDefaultsController

class GeneratedItem(object):
    OBJC_CLASS = 'NSObject'
    # This is a shorthand for setting the self.properties dictionary with the value of the prop in
    # generateInit(). This list contains either Property instances or, to avoid unnecessary
    # verbosity, a string with the property name, which is the equivalent of Property(name).
    PROPERTIES = []
    
    def __init__(self):
        self.creationOrder = globalvars.globalGenerationCounter.creationToken()
        # In case we are never assigned to a top level variable and thus never given a varname
        self.varname = "_tmp%d" % self.creationOrder
        # properties to be set at generation time. For example, if "editable" is set to False,
        # a "[$varname$ setEditable:NO];" statement will be generated.
        self.properties = {}
        self._bindings = []
    
    #--- Private
    def _generateProperties(self, properties=None):
        result = ''
        if properties is None:
            properties = self.properties
            for prop in self.PROPERTIES:
                if not isinstance(prop, Property):
                    assert isinstance(prop, str)
                    prop = Property(prop)
                prop.setOnTarget(self)
        for key, value in properties.items():
            if value is None:
                continue
            dot_elements = key.split('.')
            accessor = self.accessor
            for dot_element in dot_elements[:-1]:
                accessor = getattr(accessor, dot_element)
            if isinstance(value, GeneratedItem) and not value.generated:
                # Generate an assignment (which is generated by the "value" part of the assignment)
                # so that we set that value after our target item was generated
                setattr(accessor, dot_elements[-1], value)
            else:
                methname = 'set' + upFirstLetter(dot_elements[-1])
                result += accessor._callMethod(methname, value)
        return result
    
    #--- Virtual
    def generateInit(self):
        tmpl = CodeTemplate("$allocinit$\n$setup$\n$setprop$\n")
        tmpl.varname = self.varname
        tmpl.classname = self.OBJC_CLASS
        tmpl.allocinit = "$classname$ *$varname$ = [[[$classname$ alloc] $initmethod$] autorelease];"
        tmpl.initmethod = "init"
        tmpl.setup = ''
        return tmpl
    
    def dependencies(self):
        # Return a list of items on which self depends. We'll make sure that they're generated first.
        return []
    
    #--- Public
    @property
    def accessor(self):
        return KeyValueId(None, self.varname)
    
    @property
    def generated(self):
        return globalvars.globalGenerationCounter.isGenerated(self)
    
    def bind(self, name, target, keyPath, valueTransformer=None):
        options = {}
        if valueTransformer:
            options[const.NSValueTransformerNameBindingOption] = NLSTR(valueTransformer)
        binding = Binding(NLSTR(name), target, NLSTR(keyPath), options)
        self._bindings.append(binding)
    
    def objcValue(self):
        return self.varname
    
    def generateAssignments(self):
        if self not in KeyValueId.VALUE2KEYS:
            return ""
        assignments = []
        for key in KeyValueId.VALUE2KEYS[self]:
            setmethod = 'set' + upFirstLetter(key._name)
            assignment = key._parent._callMethod(setmethod, self)
            assignments.append(assignment)
        return '\n'.join(assignments)
    
    def generateBindings(self):
        bindings = []
        for binding in self._bindings:
            method = '[{} bind:{} toObject:{} withKeyPath:{} options:{}];'
            if binding.options:
                options = generateDictionary(binding.options)
            else:
                options = 'nil'
            name = convertValueToObjc(binding.name)
            target = convertValueToObjc(binding.target)
            keyPath = convertValueToObjc(binding.keyPath)
            bindings.append(method.format(self.varname, name, target, keyPath, options))
        return '\n'.join(bindings)
    
    def generateFinalize(self):
        # Called after everything has been generated.
        pass
    
    def generate(self, *args, **kwargs):
        result = ''
        for dependency in self.dependencies():
            if isinstance(dependency, GeneratedItem) and not dependency.generated:
                result += dependency.generate()
        inittmpl = self.generateInit(*args, **kwargs)
        inittmpl.setprop = self._generateProperties()
        result += inittmpl.render()
        result += self.generateAssignments()
        if not globalvars.globalRunMode:
            # We don't generate bindings in "run" mode because bindings can generate crashes if they
            # aren't actually connected to something.
            result += self.generateBindings()
        globalvars.globalGenerationCounter.addGenerated(self)
        return result
    

class GenerationCounter(object):
    def __init__(self):
        self.reset()
    
    def creationToken(self):
        count = self.creationCount
        self.creationCount += 1
        return count
    
    def addGenerated(self, item):
        self.generatedItems.add(item)
    
    def isGenerated(self, item):
        return item in self.generatedItems
    
    def reset(self):
        self.creationCount = 0
        self.generatedItems = set()
    

globalvars.globalGenerationCounter = GenerationCounter()