/*
 * 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.maven.doxia.macro.snippet;

import javax.inject.Named;
import javax.inject.Singleton;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import org.apache.maven.doxia.macro.AbstractMacro;
import org.apache.maven.doxia.macro.MacroExecutionException;
import org.apache.maven.doxia.macro.MacroRequest;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A macro that prints out the (source code) content of a file or a URL.
 */
@Singleton
@Named("snippet")
public class SnippetMacro extends AbstractMacro {
    private static final Logger LOGGER = LoggerFactory.getLogger(SnippetMacro.class);

    /**
     * Holds the cache.
     */
    private static Map<String, String> cache = new HashMap<>();

    private static final int HOUR = 60;

    /**
     * One hour default cache.
     */
    private long timeout = HOUR * HOUR * 1000;

    /**
     * Holds the time cache.
     */
    private static Map<String, Long> timeCached = new HashMap<>();

    /**
     * Debug.
     */
    private boolean debug = false;

    /**
     * in case of Exception during snippet download error will ignored and empty content returned.
     */
    private boolean ignoreDownloadError = true;

    /** {@inheritDoc} */
    public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
        String id = (String) request.getParameter("id");

        String urlParam = (String) request.getParameter("url");

        String fileParam = (String) request.getParameter("file");

        String debugParam = (String) request.getParameter("debug");

        if (debugParam != null) {
            this.debug = Boolean.parseBoolean(debugParam);
        }

        String ignoreDownloadErrorParam = (String) request.getParameter("ignoreDownloadError");

        if (ignoreDownloadErrorParam != null) {
            this.ignoreDownloadError = Boolean.parseBoolean(ignoreDownloadErrorParam);
        }

        boolean verbatim = true;

        String verbatimParam = (String) request.getParameter("verbatim");

        if (verbatimParam != null && !"".equals(verbatimParam)) {
            verbatim = Boolean.valueOf(verbatimParam);
        }

        boolean source = true;

        String sourceParam = (String) request.getParameter("source");

        if (sourceParam != null && !"".equals(sourceParam)) {
            source = Boolean.valueOf(sourceParam);
        }

        String encoding = (String) request.getParameter("encoding");

        URL url;

        if (!(urlParam == null || urlParam.isEmpty())) {
            try {
                url = new URL(urlParam);
            } catch (MalformedURLException e) {
                throw new IllegalArgumentException(urlParam + " is a malformed URL", e);
            }
        } else if (!(fileParam == null || fileParam.isEmpty())) {
            File f = new File(fileParam);

            if (!f.isAbsolute()) {
                f = new File(request.getBasedir(), fileParam);
            }

            try {
                url = f.toURI().toURL();
            } catch (MalformedURLException e) {
                throw new IllegalArgumentException(fileParam + " is a malformed URL", e);
            }
        } else {
            throw new IllegalArgumentException("Either the 'url' or the 'file' param has to be provided");
        }

        StringBuffer snippet;

        try {
            snippet = getSnippet(url, encoding, id);
        } catch (IOException e) {
            throw new MacroExecutionException("Error reading snippet", e);
        }

        if (verbatim) {
            sink.verbatim(source ? SinkEventAttributeSet.SOURCE : null);

            sink.text(snippet.toString());

            sink.verbatim_();
        } else {
            sink.rawText(snippet.toString());
        }
    }

    /**
     * Return a snippet of the given url.
     *
     * @param url The URL to parse.
     * @param encoding The encoding of the URL to parse.
     * @param id  The id of the snippet.
     * @return The snippet.
     * @throws IOException if something goes wrong.
     */
    private StringBuffer getSnippet(URL url, String encoding, String id) throws IOException {
        StringBuffer result;

        String cachedSnippet = getCachedSnippet(url, id);

        if (cachedSnippet != null) {
            result = new StringBuffer(cachedSnippet);

            if (debug) {
                result.append("(Served from cache)");
            }
        } else {
            try {
                result = new SnippetReader(url, encoding).readSnippet(id);
                cacheSnippet(url, id, result.toString());
                if (debug) {
                    result.append("(Fetched from url, cache content ")
                            .append(cache)
                            .append(")");
                }
            } catch (IOException e) {
                if (ignoreDownloadError) {
                    LOGGER.debug("Exception while reading '{}'", url, e);
                    result = new StringBuffer("Error during retrieving content skip as ignoreDownloadError activated.");
                } else {
                    throw e;
                }
            }
        }
        return result;
    }

    /**
     * Return a snippet from the cache.
     *
     * @param url The URL to parse.
     * @param id  The id of the snippet.
     * @return The snippet.
     */
    private String getCachedSnippet(URL url, String id) {
        if (isCacheTimedout(url, id)) {
            removeFromCache(url, id);
        }
        return cache.get(globalSnippetId(url, id));
    }

    /**
     * Return true if the snippet has been cached longer than
     * the current timeout.
     *
     * @param url The URL to parse.
     * @param id  The id of the snippet.
     * @return True if timeout exceeded.
     */
    boolean isCacheTimedout(URL url, String id) {
        return timeInCache(url, id) >= timeout;
    }

    /**
     * Return the time the snippet has been cached.
     *
     * @param url The URL to parse.
     * @param id  The id of the snippet.
     * @return The cache time.
     */
    long timeInCache(URL url, String id) {
        return System.currentTimeMillis() - getTimeCached(url, id);
    }

    /**
     * Return the absolute value of when the snippet has been cached.
     *
     * @param url The URL to parse.
     * @param id  The id of the snippet.
     * @return The cache time.
     */
    long getTimeCached(URL url, String id) {
        String globalId = globalSnippetId(url, id);

        return timeCached.containsKey(globalId) ? timeCached.get(globalId) : 0;
    }

    /**
     * Removes the snippet from the cache.
     *
     * @param url The URL to parse.
     * @param id  The id of the snippet.
     */
    private void removeFromCache(URL url, String id) {
        String globalId = globalSnippetId(url, id);

        timeCached.remove(globalId);

        cache.remove(globalId);
    }

    /**
     * Return a global identifier for the snippet.
     *
     * @param url The URL to parse.
     * @param id  The id of the snippet.
     * @return An identifier, concatenated url and id,
     *         or just url.toString() if id is empty or null.
     */
    private String globalSnippetId(URL url, String id) {
        if (id == null || id.isEmpty()) {
            return url.toString();
        }

        return url + " " + id;
    }

    /**
     * Puts the given snippet into the cache.
     *
     * @param url     The URL to parse.
     * @param id      The id of the snippet.
     * @param content The content of the snippet.
     */
    public void cacheSnippet(URL url, String id, String content) {
        cache.put(globalSnippetId(url, id), content);

        timeCached.put(globalSnippetId(url, id), System.currentTimeMillis());
    }

    /**
     * Set the cache timeout.
     *
     * @param time The timeout to set.
     */
    public void setCacheTimeout(int time) {
        this.timeout = time;
    }
}
