/*
 * Copyright 2004-2005 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.codehaus.groovy.grails.plugins

import grails.util.GrailsUtil

import org.codehaus.groovy.grails.commons.*
import org.codehaus.groovy.grails.support.SoftThreadLocalMap
import org.codehaus.groovy.grails.validation.GrailsDomainClassValidator

import org.springframework.beans.BeanUtils
import org.springframework.beans.factory.config.MethodInvokingFactoryBean
import org.springframework.context.ApplicationContext
import org.springframework.validation.BeanPropertyBindingResult
import org.springframework.validation.Errors

/**
 * A plugin that configures the domain classes in the spring context.
 *
 * @author Graeme Rocher
 * @since 0.4
 */
class DomainClassGrailsPlugin {

    def version = GrailsUtil.getGrailsVersion()
    def dependsOn = [i18n:version]
    def loadAfter = ['controllers']

    def doWithSpring = {
        for (dc in application.domainClasses) {
            // Note the use of Groovy's ability to use dynamic strings in method names!
            "${dc.fullName}"(dc.clazz) { bean ->
                bean.singleton = false
                bean.autowire = "byName"
            }
            "${dc.fullName}DomainClass"(MethodInvokingFactoryBean) { bean ->
                targetObject = ref("grailsApplication", true)
                targetMethod = "getArtefact"
                bean.lazyInit = true
                arguments = [DomainClassArtefactHandler.TYPE, dc.fullName]
            }
            "${dc.fullName}PersistentClass"(MethodInvokingFactoryBean) { bean ->
                targetObject = ref("${dc.fullName}DomainClass")
                bean.lazyInit = true
                targetMethod = "getClazz"
            }
            "${dc.fullName}Validator"(GrailsDomainClassValidator) { bean ->
                messageSource = ref("messageSource")
                bean.lazyInit = true
                domainClass = ref("${dc.fullName}DomainClass")
                grailsApplication = ref("grailsApplication", true)
            }
        }
    }

    static final PROPERTY_INSTANCE_MAP = new SoftThreadLocalMap()

    def doWithDynamicMethods = { ApplicationContext ctx->
        enhanceDomainClasses(application, ctx)
    }

    def onConfigChange = {
        application.domainClasses*.refreshConstraints()
    }

    static enhanceDomainClasses(GrailsApplication application, ApplicationContext ctx) {
        for (GrailsDomainClass dc in application.domainClasses) {
            def domainClass = dc
            MetaClass metaClass = domainClass.metaClass

            metaClass.ident = {-> delegate[domainClass.identifier.name] }
            metaClass.constructor = { ->
                if (ctx.containsBean(domainClass.fullName)) {
                    ctx.getBean(domainClass.fullName)
                }
                else {
                    BeanUtils.instantiateClass(domainClass.clazz)
                }
            }
            metaClass.static.create = { ->
                if (ctx.containsBean(domainClass.fullName)) {
                    ctx.getBean(domainClass.fullName)
                }
                else {
                    BeanUtils.instantiateClass(domainClass.clazz)
                }
            }

            addValidationMethods(application, domainClass, ctx)
            addRelationshipManagementMethods(domainClass)
        }
    }

    private static addValidationMethods(GrailsApplication application, GrailsDomainClass dc, ApplicationContext ctx) {
        def metaClass = dc.metaClass
        def domainClass = dc

        registerConstraintsProperty(metaClass, domainClass)

        metaClass.hasErrors = {-> delegate.errors?.hasErrors() }

        def get
        def put
        try {
            def rch = application.classLoader.loadClass("org.springframework.web.context.request.RequestContextHolder")
            get = {
                def attributes = rch.getRequestAttributes()
                if (attributes) {
                    return attributes.request.getAttribute(it)
                }
                return PROPERTY_INSTANCE_MAP.get().get(it)
            }
            put = { key, val ->
                def attributes = rch.getRequestAttributes()
                if (attributes) {
                    attributes.request.setAttribute(key,val)
                }
                else {
                    PROPERTY_INSTANCE_MAP.get().put(key,val)
                }
            }
        }
        catch (Throwable e) {
            get = { PROPERTY_INSTANCE_MAP.get().get(it) }
            put = { key, val -> PROPERTY_INSTANCE_MAP.get().put(key,val) }
        }

        metaClass.getErrors = { ->
            def errors
            def key = "org.codehaus.groovy.grails.ERRORS_${delegate.class.name}_${System.identityHashCode(delegate)}"
            errors = get(key)
            if (!errors) {
                errors =  new BeanPropertyBindingResult( delegate, delegate.getClass().getName())
                put key, errors
            }
            errors
        }
        metaClass.setErrors = { Errors errors ->
            def key = "org.codehaus.groovy.grails.ERRORS_${delegate.class.name}_${System.identityHashCode(delegate)}"
            put key, errors
        }
        metaClass.clearErrors = { ->
            delegate.setErrors (new BeanPropertyBindingResult(delegate, delegate.getClass().getName()))
        }
        if (!domainClass.hasMetaMethod("validate")) {
            metaClass.validate = { ->
                DomainClassPluginSupport.validateInstance(delegate, ctx)
            }
        }
    }

    /**
     * Registers the constraints property for the given MetaClass and domainClass instance
     */
    static void registerConstraintsProperty(MetaClass metaClass, GrailsDomainClass domainClass) {
        metaClass.'static'.getConstraints = { -> domainClass.constrainedProperties }

        metaClass.getConstraints = {-> domainClass.constrainedProperties }
    }

    private static addRelationshipManagementMethods(GrailsDomainClass dc) {
        def metaClass = dc.metaClass
        for (p in dc.persistantProperties) {
            def prop = p
            if (prop.basicCollectionType) {
                def collectionName = GrailsClassUtils.getClassNameRepresentation(prop.name)
                metaClass."addTo$collectionName" = { obj ->
                    if (obj instanceof CharSequence && !(obj instanceof String)) {
                        obj = obj.toString()
                    }
                    if (prop.referencedPropertyType.isInstance(obj)) {
                        if (delegate[prop.name] == null) {
                            delegate[prop.name] = GrailsClassUtils.createConcreteCollection(prop.type)
                        }
                        delegate[prop.name] << obj
                        return delegate
                    }
                    else {
                        throw new MissingMethodException("addTo${collectionName}", dc.clazz, [obj] as Object[])
                    }
                }
                metaClass."removeFrom$collectionName" = { obj ->
                    if (delegate[prop.name]) {
                        if (obj instanceof CharSequence && !(obj instanceof String)) {
                            obj = obj.toString()
                        }
                        delegate[prop.name].remove(obj)
                    }
                    return delegate
                }
            }
            else if (prop.oneToOne || prop.manyToOne) {
                def identifierPropertyName = "${prop.name}Id"
                if (!dc.hasMetaProperty(identifierPropertyName)) {
                    def getterName = GrailsClassUtils.getGetterName(identifierPropertyName)
                    metaClass."$getterName" = {-> GrailsDomainConfigurationUtil.getAssociationIdentifier(
                        delegate, prop.name, prop.referencedDomainClass) }
                }
            }
            else if (prop.oneToMany || prop.manyToMany) {
                if (metaClass instanceof ExpandoMetaClass) {
                    def propertyName = prop.name
                    def collectionName = GrailsClassUtils.getClassNameRepresentation(propertyName)
                    def otherDomainClass = prop.referencedDomainClass

                    metaClass."addTo${collectionName}" = { Object arg ->
                        Object obj
                        if (delegate[prop.name] == null) {
                            delegate[prop.name] = GrailsClassUtils.createConcreteCollection(prop.type)
                        }
                        if (arg instanceof Map) {
                            obj = otherDomainClass.newInstance()
                            obj.properties = arg
                            delegate[prop.name].add(obj)
                        }
                        else if (otherDomainClass.clazz.isInstance(arg)) {
                            obj = arg
                            delegate[prop.name].add(obj)
                        }
                        else {
                            throw new MissingMethodException("addTo${collectionName}", dc.clazz, [arg] as Object[])
                        }
                        if (prop.bidirectional && prop.otherSide) {
                            def otherSide = prop.otherSide
                            if (otherSide.oneToMany || otherSide.manyToMany) {
                                String name = prop.otherSide.name
                                if (!obj[name]) {
                                    obj[name] = GrailsClassUtils.createConcreteCollection(prop.otherSide.type)
                                }
                                obj[prop.otherSide.name].add(delegate)
                            }
                            else {
                                obj[prop.otherSide.name] = delegate
                            }
                        }
                        delegate
                    }
                    metaClass."removeFrom${collectionName}" = {Object arg ->
                        if (otherDomainClass.clazz.isInstance(arg)) {
                            delegate[prop.name]?.remove(arg)
                            if (prop.bidirectional) {
                                if (prop.manyToMany) {
                                    String name = prop.otherSide.name
                                    arg[name]?.remove(delegate)
                                }
                                else {
                                    arg[prop.otherSide.name] = null
                                }
                            }
                        }
                        else {
                            throw new MissingMethodException("removeFrom${collectionName}", dc.clazz, [arg] as Object[])
                        }
                        delegate
                    }
                }
            }
        }
    }
}
