/*
 * $Id: OptionsRenderer.java 1486577 2013-05-27 11:16:52Z mck $
 *
 * 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.tiles.extras.renderer;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.apache.tiles.Attribute;
import org.apache.tiles.ListAttribute;
import org.apache.tiles.access.TilesAccess;
import org.apache.tiles.request.ApplicationContext;
import org.apache.tiles.request.Request;
import org.apache.tiles.request.render.Renderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides a custom "options" syntax for attributes.
 * The first option that can be rendered is.
 * Comes from <a href="http://tech.finn.no/the-ultimate-view/">The Ultimate View</a> article.<p/>
 *
 * Actual rendering is delegated to the TypeDetectingRenderer that's supplied in the constructor.<p/>
 *
 * For example:
 * "/WEB-INF/tiles/fragments/${options[myoptions]}/content.jsp"
 * given the myptions list-attribute is defined like:
 * <pre>
        &lt;put-list-attribute name="myoptions">
            &lt;add-list-attribute>
                &lt;add-attribute value="car"/>
                &lt;add-attribute value="vechile"/>
                &lt;add-attribute value="advert"/>
            &lt;/add-list-attribute>
        &lt;/put-list-attribute>
   </pre>
 * will look for content.jsp <br/>
 * first in "/WEB-INF/tiles/fragments/car/" then <br/>
 * second in "/WEB-INF/tiles/fragments/vechile/" and <br/>
 * last in "/WEB-INF/tiles/fragments/advert".
 * <p/>
 * <p/>
 * Currently only supports one occurrance of such an "option" pattern in the attribute's value.
 * <p/>
 * Limitation: "looking" for templates is implemented using applicationContext.getResource(..)
 * therefore the option values in the options list need to be visible as applicationResources.
 * <p/>
 * The attribute found and rendered is cached so to improve performance on subsequent lookups.
 * The default cache time-to-live is {@value #DEFAULT_CACHE_LIFE}, specified by {@link #DEFAULT_CACHE_LIFE}.
 * It can be customised by setting the system property {@value #CACHE_LIFE_PROPERTY}, see {@link #CACHE_LIFE_PROPERTY}.
 * Setting it to zero will disable the cache.
 */
public final class OptionsRenderer implements Renderer {

    public static final String CACHE_LIFE_PROPERTY = OptionsRenderer.class.getName() + ".cache_ttl_ms";

    public static final long DEFAULT_CACHE_LIFE = 1000 * 60 * 5;

    public static final Pattern OPTIONS_PATTERN
            = Pattern.compile(Pattern.quote("{options[") + "(.+)" + Pattern.quote("]}"));

    private static final Logger LOG = LoggerFactory.getLogger(OptionsRenderer.class);

    private final ApplicationContext applicationContext;
    private final Renderer renderer;

    public OptionsRenderer(final ApplicationContext applicationContext, final Renderer renderer) {
        this.applicationContext = applicationContext;
        this.renderer = renderer;
    }

    @Override
    public boolean isRenderable(final String path, final Request request) {
        return renderer.isRenderable(path, request);
    }

    @Override
    public void render(final String path, final Request request) throws IOException {

        Matcher matcher =  OPTIONS_PATTERN.matcher((String) path);

        if (null != matcher && matcher.find()) {
            boolean done = false;
            String match = matcher.group(1);
            ListAttribute fallbacks = (ListAttribute) TilesAccess
                    .getCurrentContainer(request)
                    .getAttributeContext(request)
                    .getAttribute(match);

            if (null == fallbacks) {
                throw new IllegalStateException("A matching list-attribute name=\"" + match + "\" must be defined.");
            } else if (fallbacks.getValue().isEmpty()) {
                throw new IllegalStateException(
                        "list-attribute name=\"" + match + "\" must have minimum one attribute");
            }

            for (Attribute option : (List<Attribute>) fallbacks.getValue()) {
                String template = path.replaceFirst(Pattern.quote(matcher.group()), (String) option.getValue());
                done = renderAttempt(template, request);
                if (done) { break; }
            }
            if (!done) {
                throw new IOException("None of the options existed for " + path);
            }
        } else {
            renderer.render(path, request);
        }
    }

    private boolean renderAttempt(final String template, final Request request) throws IOException {
        boolean result = false;
        if (Cache.attemptTemplate(template)) {
            try {
                if (null != applicationContext.getResource(template)) {
                    renderer.render(template, request);
                    result = true;
                }
            } catch (IOException ex) {
                if (ex.getMessage().contains(template)) {
                    // expected outcome. continue loop.
                    LOG.trace(ex.getMessage());
                } else {
                    // comes from an inner templateAttribute.render(..) so throw on
                    throw ex;
                }
            } catch (RuntimeException ex) {
                if (ex.getMessage().contains(template)) {
                    // expected outcome. continue loop.
                    LOG.trace(ex.getMessage());
                } else {
                    // comes from an inner templateAttribute.render(..) so throw on
                    throw ex;
                }
            }
            Cache.update(template, result);
        }
        return result;
    }

    private static final class Cache {

        private static final long CACHE_LIFE = Long.getLong(CACHE_LIFE_PROPERTY, DEFAULT_CACHE_LIFE);

        /** It takes CACHE_LIFE milliseconds for any hot deployments to register.
         */
        private static final ConcurrentMap<String,Boolean> TEMPLATE_EXISTS;
        
        static {
            LOG.info("cache_ttl_ms=" + CACHE_LIFE);

            LoadingCache<String,Boolean> builder = CacheBuilder
                    .newBuilder()
                    .expireAfterWrite(CACHE_LIFE, TimeUnit.MILLISECONDS)
                    .build(
                        new CacheLoader<String, Boolean>() {
                            @Override
                            public Boolean load(String key) {
                                throw new UnsupportedOperationException(
                                        "illegal TEMPLATE_EXISTS.get(\"" + key
                                        + "\") before TEMPLATE_EXISTS.containsKey(\"" + key + "\")");
                            }
                        });

            TEMPLATE_EXISTS = builder.asMap();
        }


        static boolean attemptTemplate(final String template) {
            return !TEMPLATE_EXISTS.containsKey(template) || TEMPLATE_EXISTS.get(template);
        }

        static void update(final String template, final boolean found) {
            TEMPLATE_EXISTS.putIfAbsent(template, found);
        }

        private Cache() {}
    }
}
