/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.brooklyn.util.core.text;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Map;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.drivers.EntityDriver;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.location.internal.LocationInternal;
import org.apache.brooklyn.core.sensor.DependentConfiguration;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Charsets;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;

import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.MapKeyValuePairIterator;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleCollection;
import freemarker.template.Template;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateHashModelEx2;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;

/** A variety of methods to assist in Freemarker template processing,
 * including passing in maps with keys flattened (dot-separated namespace),
 * and accessing {@link ManagementContext} brooklyn.properties 
 * and {@link Entity}, {@link EntityDriver}, and {@link Location} methods and config.
 * <p>
 * See {@link #processTemplateContents(String, ManagementContext, Map)} for
 * a description of how management access is done.
 */
public class TemplateProcessor {

    private static final Logger log = LoggerFactory.getLogger(TemplateProcessor.class);
    private static final ObjectWrapper WRAPPER =
            new DefaultObjectWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build();

    protected static TemplateModel wrapAsTemplateModel(Object o) throws TemplateModelException {
        if (o instanceof Map) return new DotSplittingTemplateModel((Map<?,?>)o);
        return WRAPPER.wrap(o);
    }
    
    /** As per {@link #processTemplateContents(String, Map)}, but taking a file. */
    public static String processTemplateFile(String templateFileName, Map<String, ? extends Object> substitutions) {
        String templateContents;
        try {
            templateContents = Files.toString(new File(templateFileName), Charsets.UTF_8);
        } catch (IOException e) {
            log.warn("Error loading file " + templateFileName, e);
            throw Exceptions.propagate(e);
        }
        return processTemplateContents(templateContents, substitutions);
    }

    /** Processes template contents according to {@link EntityAndMapTemplateModel}. */
    public static String processTemplateFile(String templateFileName, EntityDriver driver, Map<String, ? extends Object> extraSubstitutions) {
        String templateContents;
        try {
            templateContents = Files.toString(new File(templateFileName), Charsets.UTF_8);
        } catch (IOException e) {
            log.warn("Error loading file " + templateFileName, e);
            throw Exceptions.propagate(e);
        }
        return processTemplateContents(templateContents, driver, extraSubstitutions);
    }

    /** Processes template contents according to {@link EntityAndMapTemplateModel}. */
    public static String processTemplateContents(String templateContents, EntityDriver driver, Map<String,? extends Object> extraSubstitutions) {
        return processTemplateContents(templateContents, new EntityAndMapTemplateModel(driver, extraSubstitutions));
    }

    /** Processes template contents according to {@link EntityAndMapTemplateModel}. */
    public static String processTemplateContents(String templateContents, ManagementContext managementContext, Map<String,? extends Object> extraSubstitutions) {
        return processTemplateContents(templateContents, new EntityAndMapTemplateModel(managementContext, extraSubstitutions));
    }

    /** Processes template contents according to {@link EntityAndMapTemplateModel}. */
    public static String processTemplateContents(String templateContents, Location location, Map<String,? extends Object> extraSubstitutions) {
        return processTemplateContents(templateContents, new LocationAndMapTemplateModel((LocationInternal)location, extraSubstitutions));
    }

    /**
     * A Freemarker {@link TemplateHashModel} which will correctly handle entries of the form "a.b" in this map,
     * matching against template requests for "${a.b}".
     * <p>
     * Freemarker requests "a" in a map when given such a request, and expects that to point to a map
     * with a key "b". This model provides such maps even for "a.b" in a map.
     * <p>
     * However if "a" <b>and</b> "a.b" are in the map, this will <b>not</b> currently do the deep mapping.
     * (It does not have enough contextual information from Freemarker to handle this case.) */
    public static final class DotSplittingTemplateModel implements TemplateHashModelEx2 {
        protected final Map<?,?> map;

        protected DotSplittingTemplateModel(Map<?,?> map) {
            this.map = map;
        }

        @Override
        public boolean isEmpty() { return map!=null && map.isEmpty(); }

        public boolean contains(String key) {
            if (map==null) return false;
            if (map.containsKey(key)) return true;
            for (Map.Entry<?,?> entry: map.entrySet()) {
                String k = Strings.toString(entry.getKey());
                if (k.startsWith(key+".")) {
                    // contains this prefix
                    return true;
                }
            }
            return false;
        }
        
        @Override
        public TemplateModel get(String key) {
            if (map==null) return null;
            try {
                if (map.containsKey(key)) 
                    return wrapAsTemplateModel( map.get(key) );
                
                Map<String,Object> result = MutableMap.of();
                for (Map.Entry<?,?> entry: map.entrySet()) {
                    String k = Strings.toString(entry.getKey());
                    if (k.startsWith(key+".")) {
                        String k2 = Strings.removeFromStart(k, key+".");
                        result.put(k2, entry.getValue());
                    }
                }
                if (!result.isEmpty()) 
                        return wrapAsTemplateModel( result );
                
            } catch (Exception e) {
                Exceptions.propagateIfFatal(e);
                throw new IllegalStateException("Error accessing config '"+key+"'"+": "+e, e);
            }
            
            return null;
        }
        
        @Override
        public String toString() {
            return getClass().getName()+"["+map+"]";
        }

        public int size() {
            return map.size();
        }

        public TemplateCollectionModel keys() {
            return new SimpleCollection(map.keySet(), WRAPPER);
        }

        public TemplateCollectionModel values() {
            return new SimpleCollection(map.values(), WRAPPER);
        }

        public KeyValuePairIterator keyValuePairIterator() {
            return new MapKeyValuePairIterator(map, WRAPPER);
        }
    }
    
    /** FreeMarker {@link TemplateHashModel} which resolves keys inside the given entity or management context.
     * Callers are required to include dots for dot-separated keys.
     * Freemarker will only do this when in inside bracket notation in an outer map, as in <code>${outer['a.b.']}</code>; 
     * as a result this is intended only for use by {@link EntityAndMapTemplateModel} where 
     * a caller has used bracked notation, as in <code>${mgmt['key.subkey']}</code>. */
    protected static final class EntityConfigTemplateModel implements TemplateHashModel {
        protected final EntityInternal entity;
        protected final ManagementContext mgmt;

        protected EntityConfigTemplateModel(EntityInternal entity) {
            this.entity = checkNotNull(entity, "entity");
            this.mgmt = entity.getManagementContext();
        }

        @Override
        public boolean isEmpty() { return false; }

        @Override
        public TemplateModel get(String key) throws TemplateModelException {
            try {
                Object result = entity.getConfig(ConfigKeys.builder(Object.class).name(key).build());
                
                if (result==null)
                    result = mgmt.getConfig().getConfig(ConfigKeys.builder(Object.class).name(key).build());
                
                if (result!=null)
                    return wrapAsTemplateModel( result );
                
            } catch (Exception e) {
                Exceptions.propagateIfFatal(e);
                throw new IllegalStateException("Error accessing config '"+key+"'"+" on "+entity+": "+e, e);
            }
            
            return null;
        }
        
        @Override
        public String toString() {
            return getClass().getName()+"["+entity+"]";
        }
    }

    /** FreeMarker {@link TemplateHashModel} which resolves keys inside the given management context.
     * Callers are required to include dots for dot-separated keys.
     * Freemarker will only do this when in inside bracket notation in an outer map, as in <code>${outer['a.b.']}</code>; 
     * as a result this is intended only for use by {@link EntityAndMapTemplateModel} where 
     * a caller has used bracked notation, as in <code>${mgmt['key.subkey']}</code>. */
    protected static final class MgmtConfigTemplateModel implements TemplateHashModel {
        protected final ManagementContext mgmt;

        protected MgmtConfigTemplateModel(ManagementContext mgmt) {
            this.mgmt = checkNotNull(mgmt, "mgmt");
        }

        @Override
        public boolean isEmpty() { return false; }

        @Override
        public TemplateModel get(String key) throws TemplateModelException {
            try {
                Object result = mgmt.getConfig().getConfig(ConfigKeys.builder(Object.class).name(key).build());
                
                if (result!=null)
                    return wrapAsTemplateModel( result );
                
            } catch (Exception e) {
                Exceptions.propagateIfFatal(e);
                throw new IllegalStateException("Error accessing config '"+key+"': "+e, e);
            }
            
            return null;
        }
        
        @Override
        public String toString() {
            return getClass().getName()+"["+mgmt+"]";
        }
    }
    
    /** FreeMarker {@link TemplateHashModel} which resolves keys inside the given location.
     * Callers are required to include dots for dot-separated keys.
     * Freemarker will only do this when in inside bracket notation in an outer map, as in <code>${outer['a.b.']}</code>; 
     * as a result this is intended only for use by {@link LocationAndMapTemplateModel} where 
     * a caller has used bracked notation, as in <code>${mgmt['key.subkey']}</code>. */
    protected static final class LocationConfigTemplateModel implements TemplateHashModel {
        protected final LocationInternal location;
        protected final ManagementContext mgmt;

        protected LocationConfigTemplateModel(LocationInternal location) {
            this.location = checkNotNull(location, "location");
            this.mgmt = location.getManagementContext();
        }

        @Override
        public boolean isEmpty() { return false; }

        @Override
        public TemplateModel get(String key) throws TemplateModelException {
            try {
                Object result = null;
                
                result = location.getConfig(ConfigKeys.builder(Object.class).name(key).build());
                
                if (result==null && mgmt!=null)
                    result = mgmt.getConfig().getConfig(ConfigKeys.builder(Object.class).name(key).build());
                
                if (result!=null)
                    return wrapAsTemplateModel( result );
                
            } catch (Exception e) {
                Exceptions.propagateIfFatal(e);
                throw new IllegalStateException("Error accessing config '"+key+"'"
                    + (location!=null ? " on "+location : "")+": "+e, e);
            }
            
            return null;
        }
        
        @Override
        public String toString() {
            return getClass().getName()+"["+location+"]";
        }
    }

    protected final static class EntityAttributeTemplateModel implements TemplateHashModel {
        protected final EntityInternal entity;

        protected EntityAttributeTemplateModel(EntityInternal entity) {
            this.entity = entity;
        }

        @Override
        public boolean isEmpty() throws TemplateModelException {
            return false;
        }

        @Override
        public TemplateModel get(String key) throws TemplateModelException {
            Object result;
            try {
                result = Entities.submit(entity, DependentConfiguration.attributeWhenReady(entity,
                        Sensors.builder(Object.class, key).persistence(AttributeSensor.SensorPersistenceMode.NONE).build())).get();
            } catch (Exception e) {
                throw Exceptions.propagate(e);
            }
            if (result == null) {
                return null;
            } else {
                return wrapAsTemplateModel(result);
            }
        }

        @Override
        public String toString() {
            return getClass().getName()+"["+entity+"]";
        }
    }

    /**
     * Provides access to config on an entity or management context, using
     * <code>${config['entity.config.key']}</code> or <code>${mgmt['brooklyn.properties.key']}</code> notation,
     * and also allowing access to <code>getX()</code> methods on entity (interface) or driver
     * using <code>${entity.x}</code> or <code><${driver.x}</code>.
     * Optional extra properties can be supplied, treated as per {@link DotSplittingTemplateModel}.
     */
    protected static final class EntityAndMapTemplateModel implements TemplateHashModel {
        protected final EntityInternal entity;
        protected final EntityDriver driver;
        protected final ManagementContext mgmt;
        protected final DotSplittingTemplateModel extraSubstitutionsModel;

        protected EntityAndMapTemplateModel(ManagementContext mgmt, Map<String,? extends Object> extraSubstitutions) {
            this.entity = null;
            this.driver = null;
            this.mgmt = mgmt;
            this.extraSubstitutionsModel = new DotSplittingTemplateModel(extraSubstitutions);
        }

        protected EntityAndMapTemplateModel(EntityDriver driver, Map<String,? extends Object> extraSubstitutions) {
            this.driver = driver;
            this.entity = (EntityInternal) driver.getEntity();
            this.mgmt = entity.getManagementContext();
            this.extraSubstitutionsModel = new DotSplittingTemplateModel(extraSubstitutions);
        }

        protected EntityAndMapTemplateModel(EntityInternal entity, Map<String,? extends Object> extraSubstitutions) {
            this.entity = entity;
            this.driver = null;
            this.mgmt = entity.getManagementContext();
            this.extraSubstitutionsModel = new DotSplittingTemplateModel(extraSubstitutions);
        }

        @Override
        public boolean isEmpty() { return false; }

        @Override
        public TemplateModel get(String key) throws TemplateModelException {
            if (extraSubstitutionsModel.contains(key))
                return wrapAsTemplateModel( extraSubstitutionsModel.get(key) );

            if ("entity".equals(key) && entity!=null)
                return wrapAsTemplateModel( entity );
            if ("config".equals(key)) {
                if (entity!=null)
                    return new EntityConfigTemplateModel(entity);
                else
                    return new MgmtConfigTemplateModel(mgmt);
            }
            if ("mgmt".equals(key)) {
                return new MgmtConfigTemplateModel(mgmt);
            }

            if ("driver".equals(key) && driver!=null)
                return wrapAsTemplateModel( driver );
            if ("location".equals(key)) {
                if (driver!=null && driver.getLocation()!=null)
                    return wrapAsTemplateModel( driver.getLocation() );
                if (entity!=null)
                    return wrapAsTemplateModel( Iterables.getOnlyElement( entity.getLocations() ) );
            }
            if ("attribute".equals(key)) {
                return new EntityAttributeTemplateModel(entity);
            }
            
            if (mgmt!=null) {
                // TODO deprecated in 0.7.0, remove after next version
                // ie not supported to access global props without qualification
                Object result = mgmt.getConfig().getConfig(ConfigKeys.builder(Object.class).name(key).build());
                if (result!=null) { 
                    log.warn("Deprecated access of global brooklyn.properties value for "+key+"; should be qualified with 'mgmt.'");
                    return wrapAsTemplateModel( result );
                }
            }
            
            if ("javaSysProps".equals(key))
                return wrapAsTemplateModel( System.getProperties() );

            return null;
        }
        
        @Override
        public String toString() {
            return getClass().getName()+"["+(entity!=null ? entity : mgmt)+"]";
        }
    }

    /**
     * Provides access to config on an entity or management context, using
     * <code>${config['entity.config.key']}</code> or <code>${mgmt['brooklyn.properties.key']}</code> notation,
     * and also allowing access to <code>getX()</code> methods on entity (interface) or driver
     * using <code>${entity.x}</code> or <code><${driver.x}</code>.
     * Optional extra properties can be supplied, treated as per {@link DotSplittingTemplateModel}.
     */
    protected static final class LocationAndMapTemplateModel implements TemplateHashModel {
        protected final LocationInternal location;
        protected final ManagementContext mgmt;
        protected final DotSplittingTemplateModel extraSubstitutionsModel;

        protected LocationAndMapTemplateModel(LocationInternal location, Map<String,? extends Object> extraSubstitutions) {
            this.location = checkNotNull(location, "location");
            this.mgmt = location.getManagementContext();
            this.extraSubstitutionsModel = new DotSplittingTemplateModel(extraSubstitutions);
        }

        @Override
        public boolean isEmpty() { return false; }

        @Override
        public TemplateModel get(String key) throws TemplateModelException {
            if (extraSubstitutionsModel.contains(key))
                return wrapAsTemplateModel( extraSubstitutionsModel.get(key) );

            if ("location".equals(key))
                return wrapAsTemplateModel( location );
            if ("config".equals(key)) {
                return new LocationConfigTemplateModel(location);
            }
            if ("mgmt".equals(key)) {
                return new MgmtConfigTemplateModel(mgmt);
            }

            if (mgmt!=null) {
                // TODO deprecated in 0.7.0, remove after next version
                // ie not supported to access global props without qualification
                Object result = mgmt.getConfig().getConfig(ConfigKeys.builder(Object.class).name(key).build());
                if (result!=null) { 
                    log.warn("Deprecated access of global brooklyn.properties value for "+key+"; should be qualified with 'mgmt.'");
                    return wrapAsTemplateModel( result );
                }
            }
            
            if ("javaSysProps".equals(key))
                return wrapAsTemplateModel( System.getProperties() );

            return null;
        }
        
        @Override
        public String toString() {
            return getClass().getName()+"["+location+"]";
        }
    }

    /** Processes template contents with the given items in scope as per {@link EntityAndMapTemplateModel}. */
    public static String processTemplateContents(String templateContents, final EntityInternal entity, Map<String,? extends Object> extraSubstitutions) {
        return processTemplateContents(templateContents, new EntityAndMapTemplateModel(entity, extraSubstitutions));
    }
    
    /** Processes template contents using the given map, passed to freemarker,
     * with dot handling as per {@link DotSplittingTemplateModel}. */
    public static String processTemplateContents(String templateContents, final Map<String, ? extends Object> substitutions) {
        TemplateHashModel root;
        try {
            root = substitutions != null
                ? (TemplateHashModel)wrapAsTemplateModel(substitutions)
                : null;
        } catch (TemplateModelException e) {
            throw new IllegalStateException("Unable to set up TemplateHashModel to parse template, given "+substitutions+": "+e, e);
        }
        
        return processTemplateContents(templateContents, root);
    }
    
    /** Processes template contents against the given {@link TemplateHashModel}. */
    public static String processTemplateContents(String templateContents, final TemplateHashModel substitutions) {
        try {
            Configuration cfg = new Configuration();
            StringTemplateLoader templateLoader = new StringTemplateLoader();
            templateLoader.putTemplate("config", templateContents);
            cfg.setTemplateLoader(templateLoader);
            Template template = cfg.getTemplate("config");

            // TODO could expose CAMP '$brooklyn:' style dsl, based on template.createProcessingEnvironment
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            Writer out = new OutputStreamWriter(baos);
            template.process(substitutions, out);
            out.flush();

            return new String(baos.toByteArray());
        } catch (Exception e) {
            log.warn("Error processing template (propagating): "+e, e);
            log.debug("Template which could not be parsed (causing "+e+") is:"
                + (Strings.isMultiLine(templateContents) ? "\n"+templateContents : templateContents));
            throw Exceptions.propagate(e);
        }
    }
}
