/*
 * Copyright 2013 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 grails.gsp.boot;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

import org.codehaus.groovy.grails.commons.GrailsApplication;
import org.codehaus.groovy.grails.commons.StandaloneGrailsApplication;
import org.codehaus.groovy.grails.plugins.codecs.StandaloneCodecLookup;
import org.codehaus.groovy.grails.support.encoding.CodecLookup;
import org.codehaus.groovy.grails.web.pages.GroovyPagesTemplateEngine;
import org.codehaus.groovy.grails.web.pages.GroovyPagesTemplateRenderer;
import org.codehaus.groovy.grails.web.pages.StandaloneTagLibraryLookup;
import org.codehaus.groovy.grails.web.pages.discovery.CachingGrailsConventionGroovyPageLocator;
import org.codehaus.groovy.grails.web.pages.discovery.GrailsConventionGroovyPageLocator;
import org.codehaus.groovy.grails.web.pages.discovery.GroovyPageLocator;
import org.codehaus.groovy.grails.web.pages.ext.jsp.TagLibraryResolverImpl;
import org.codehaus.groovy.grails.web.servlet.view.GrailsLayoutViewResolver;
import org.codehaus.groovy.grails.web.servlet.view.GroovyPageViewResolver;
import org.codehaus.groovy.grails.web.sitemesh.GroovyPageLayoutFinder;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ViewResolver;

@Configuration
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
public class GspAutoConfiguration {
    protected static abstract class AbstractGspConfig {
        @Value("${spring.gsp.reloadingEnabled:true}")
        boolean gspReloadingEnabled;
        
        @Value("${spring.gsp.view.cacheTimeout:1000}")
        long viewCacheTimeout;
    }
    
    @Configuration
    @Import({TagLibraryLookupRegistrar.class, RemoveDefaultViewResolverRegistrar.class})
    protected static class GspTemplateEngineAutoConfiguration extends AbstractGspConfig {
        private static final String LOCAL_DIRECTORY_TEMPLATE_ROOT="./src/main/resources/templates";
        private static final String CLASSPATH_TEMPLATE_ROOT="classpath:/templates";
        
        @Value("${spring.gsp.templateRoots:}")
        String[] templateRoots;
        
        @Value("${spring.gsp.locator.cacheTimeout:5000}")
        long locatorCacheTimeout;
        
        @Value("${spring.gsp.layout.caching:true}")
        boolean gspLayoutCaching;
        
        @Value("${spring.gsp.layout.default:main}")
        String defaultLayoutName;

        @Bean(autowire=Autowire.BY_NAME)
        @ConditionalOnMissingBean(name="groovyPagesTemplateEngine") 
        GroovyPagesTemplateEngine groovyPagesTemplateEngine() {
            GroovyPagesTemplateEngine templateEngine = new GroovyPagesTemplateEngine();
            templateEngine.setReloadEnabled(gspReloadingEnabled);
            return templateEngine;
        }
        
        @Bean(autowire=Autowire.BY_NAME)
        @ConditionalOnMissingBean(name="groovyPageLocator")
        GroovyPageLocator groovyPageLocator() {
            final List<String> templateRootsCleaned=resolveTemplateRoots();
            CachingGrailsConventionGroovyPageLocator pageLocator = new CachingGrailsConventionGroovyPageLocator() {
                protected List<String> resolveSearchPaths(String uri) {
                    List<String> paths=new ArrayList<String>(templateRootsCleaned.size());
                    for(String rootPath : templateRootsCleaned) {
                        paths.add(rootPath + cleanUri(uri));
                    }
                    return paths;
                }

                protected String cleanUri(String uri) {
                    uri = StringUtils.cleanPath(uri);
                    if(!uri.startsWith("/")) {
                        uri = "/" + uri;
                    }
                    return uri;
                }
            };
            pageLocator.setReloadEnabled(gspReloadingEnabled);
            pageLocator.setCacheTimeout(gspReloadingEnabled ? locatorCacheTimeout : -1);
            return pageLocator;
        }

        protected List<String> resolveTemplateRoots() {
            if (templateRoots.length > 0) {
                List<String> rootPaths = new ArrayList<String>(templateRoots.length);
                for (String rootPath : templateRoots) {
                    rootPath = rootPath.trim();
                    // remove trailing slash since uri will always be prefixed with a slash
                    if(rootPath.endsWith("/")) {
                        rootPath = rootPath.substring(0, rootPath.length()-1);
                    }
                    if(!StringUtils.isEmpty(rootPath)) {
                        rootPaths.add(rootPath);
                    }
                }
                return rootPaths;
            }
            else {
                if (gspReloadingEnabled) {
                    File templateRootDirectory = new File(LOCAL_DIRECTORY_TEMPLATE_ROOT);
                    if (templateRootDirectory.isDirectory()) {
                        return Collections.singletonList("file:" + LOCAL_DIRECTORY_TEMPLATE_ROOT);
                    }
                }
                return Collections.singletonList(CLASSPATH_TEMPLATE_ROOT);
            }
        }
        
        @Bean
        @ConditionalOnMissingBean(name = "groovyPageLayoutFinder")
        public GroovyPageLayoutFinder groovyPageLayoutFinder() {
            GroovyPageLayoutFinder groovyPageLayoutFinder = new GroovyPageLayoutFinder();
            groovyPageLayoutFinder.setGspReloadEnabled(gspReloadingEnabled);
            groovyPageLayoutFinder.setCacheEnabled(gspLayoutCaching);
            groovyPageLayoutFinder.setEnableNonGspViews(false);
            groovyPageLayoutFinder.setDefaultDecoratorName(defaultLayoutName);
            return groovyPageLayoutFinder;
        }
        
        @Bean(autowire=Autowire.BY_NAME)
        @ConditionalOnMissingBean(name = "groovyPagesTemplateRenderer")
        GroovyPagesTemplateRenderer groovyPagesTemplateRenderer() {
            GroovyPagesTemplateRenderer groovyPagesTemplateRenderer = new GroovyPagesTemplateRenderer();
            groovyPagesTemplateRenderer.setCacheEnabled(!gspReloadingEnabled);
            return groovyPagesTemplateRenderer;
        }
    }
    
    @Configuration
    protected static class GspViewResolverConfiguration extends AbstractGspConfig {
        @Autowired
        GroovyPagesTemplateEngine groovyPagesTemplateEngine;
        
        @Autowired
        GrailsConventionGroovyPageLocator groovyPageLocator;
        
        @Autowired
        GroovyPageLayoutFinder groovyPageLayoutFinder;
        
        @Bean
        @ConditionalOnMissingBean(name = "gspViewResolver")
        public GrailsLayoutViewResolver gspViewResolver() {
            return new GrailsLayoutViewResolver(innerGspViewResolver(), groovyPageLayoutFinder);
        }

        ViewResolver innerGspViewResolver() {
            GroovyPageViewResolver innerGspViewResolver = new GroovyPageViewResolver(groovyPagesTemplateEngine, groovyPageLocator);
            innerGspViewResolver.setAllowGrailsViewCaching(!gspReloadingEnabled || viewCacheTimeout != 0);
            innerGspViewResolver.setCacheTimeout(gspReloadingEnabled ? viewCacheTimeout : -1);
            return innerGspViewResolver;
        }
    }
    
    @Configuration 
    protected static class CodecLookupConfiguration {
        @Bean
        @ConditionalOnMissingBean(name = "codecLookup")
        public CodecLookup codecLookup() {
            return new StandaloneCodecLookup();
        }
    }
    
    @Configuration
    protected static class StandaloneGrailsApplicationConfiguration {
        @Bean
        @ConditionalOnMissingBean(name = "grailsApplication") 
        public GrailsApplication grailsApplication() {
            return new SpringBootGrailsApplication();
        }
    }
    
    /**
     * Makes Spring Boot application properties available in the GrailsApplication instance's flatConfig
     *
     */
    public static class SpringBootGrailsApplication extends StandaloneGrailsApplication implements EnvironmentAware {
        private Environment environment;
        
        @SuppressWarnings({ "rawtypes", "unchecked" })
        @Override
        public void updateFlatConfig() {
            super.updateFlatConfig();
            if(this.environment instanceof ConfigurableEnvironment) {
                ConfigurableEnvironment configurableEnv = ((ConfigurableEnvironment)environment);
                for(PropertySource<?> propertySource : configurableEnv.getPropertySources()) {
                    if(propertySource instanceof EnumerablePropertySource) {
                        EnumerablePropertySource<?> enumerablePropertySource = (EnumerablePropertySource)propertySource;
                        for(String propertyName : enumerablePropertySource.getPropertyNames()) {
                            flatConfig.put(propertyName, enumerablePropertySource.getProperty(propertyName));
                        }
                    }
                }
            }
        }
        
        @Override
        public void setEnvironment(Environment environment) {
            this.environment = environment;
            updateFlatConfig();
        }
    }
    
    protected static class TagLibraryLookupRegistrar implements ImportBeanDefinitionRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            if(!registry.containsBeanDefinition("gspTagLibraryLookup")) {
                GenericBeanDefinition beanDefinition = createBeanDefinition(StandaloneTagLibraryLookup.class);
                
                ManagedList<BeanDefinition> list = new ManagedList<BeanDefinition>();
                registerTagLibs(list);
                
                beanDefinition.getPropertyValues().addPropertyValue("tagLibInstances", list);
                
                registry.registerBeanDefinition("gspTagLibraryLookup", beanDefinition);
                registry.registerAlias("gspTagLibraryLookup", "tagLibraryLookup");
            }
        }

        protected void registerTagLibs(ManagedList<BeanDefinition> list) {
            for(Class<?> taglibClazz : StandaloneTagLibraryLookup.DEFAULT_TAGLIB_CLASSES) {
                list.add(createBeanDefinition(taglibClazz));
            }
        }

        protected GenericBeanDefinition createBeanDefinition(Class<?> beanClass) {
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(beanClass);
            beanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_NAME);
            return beanDefinition;
        }
    }
    
    /**
     * {@link WebMvcAutoConfiguration} adds defaultViewResolver and viewResolver beans.
     * 
     *  This ImportBeanDefinitionRegistrar removes the defaultViewResolver and replaces 
     *  the viewResolver bean with GSP view resolver by default.
     *  
     *  The behavior of this class can be controlled with spring.gsp.removeDefaultViewResolver and
     *  spring.gsp.replaceViewResolverBean configuration properties.
     *
     */
    protected static class RemoveDefaultViewResolverRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
        boolean removeDefaultViewResolverBean;
        boolean replaceViewResolverBean;
        
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            if(removeDefaultViewResolverBean) {
                if(registry.containsBeanDefinition("defaultViewResolver")) {
                    registry.removeBeanDefinition("defaultViewResolver");
                }
            }
            if(replaceViewResolverBean) {
                if(registry.containsBeanDefinition("viewResolver")) {
                    registry.removeBeanDefinition("viewResolver");
                }
                registry.registerAlias("gspViewResolver", "viewResolver");
            }
        }

        @Override
        public void setEnvironment(Environment environment) {
            removeDefaultViewResolverBean = environment.getProperty("spring.gsp.removeDefaultViewResolverBean", Boolean.class, true);
            replaceViewResolverBean = environment.getProperty("spring.gsp.replaceViewResolverBean", Boolean.class, true);
        }
    }
    
    @ConditionalOnClass({javax.servlet.jsp.tagext.JspTag.class, TagLibraryResolverImpl.class})
    @Configuration
    protected static class GspJspIntegrationConfiguration implements EnvironmentAware {
        @Bean(autowire = Autowire.BY_NAME)
        public TagLibraryResolverImpl jspTagLibraryResolver() {
            return new TagLibraryResolverImpl();
        }

        @Override
        public void setEnvironment(Environment environment) {
            if(environment instanceof ConfigurableEnvironment) {
                ConfigurableEnvironment configEnv = (ConfigurableEnvironment) environment;
                Properties defaultProperties = createDefaultProperties();
                configEnv.getPropertySources().addLast(new PropertiesPropertySource(GspJspIntegrationConfiguration.class.getName(), defaultProperties));
            }
        }

        protected Properties createDefaultProperties() {
            Properties defaultProperties = new Properties();
            // scan for spring JSP taglib tld files by default, also scan for 
            defaultProperties.put("spring.gsp.tldScanPattern","classpath*:/META-INF/spring*.tld,classpath*:/META-INF/fmt.tld,classpath*:/META-INF/c.tld,classpath*:/META-INF/c-1_0-rt.tld");
            return defaultProperties;
        }
    }
}