/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  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.  For additional information regarding
 * copyright in this work, please see the NOTICE file in the top level
 * directory of this distribution.
 */

package org.apache.roller.weblogger.business.themes;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import org.apache.roller.weblogger.pojos.TemplateRendition.RenditionType;
import org.apache.roller.weblogger.pojos.ThemeResource;
import org.apache.roller.weblogger.pojos.ThemeTemplate;
import org.apache.roller.weblogger.pojos.ThemeTemplate.ComponentType;

/**
 * The Theme object encapsulates all elements of a single weblog theme. It is
 * used mostly to contain all the templates for a theme, but does contain other
 * theme related attributes such as name, last modified date, etc.
 */
public class SharedThemeFromDir extends SharedTheme {

    private static Log log = LogFactory.getLog(SharedThemeFromDir.class);

    // the filesystem directory where we should read this theme from
    private String themeDir = null;

    // the theme preview image
    private ThemeResource previewImage = null;

    // the theme stylesheet
    private ThemeTemplate stylesheet = null;

    // we keep templates in a Map for faster lookups by name
    private Map<String, ThemeTemplate> templatesByName = new HashMap<>();

    // we keep templates in a Map for faster lookups by link
    private Map<String, ThemeTemplate> templatesByLink = new HashMap<>();

    // we keep templates in a Map for faster lookups by action
    private Map<ComponentType, ThemeTemplate> templatesByAction = new EnumMap<>(ComponentType.class);

    // we keep resources in a Map for faster lookups by path
    // the Map contains ... (resource path, ThemeResource)
    private Map<String, ThemeResource> resources = new HashMap<>();

    public SharedThemeFromDir(String themeDirPath)
            throws ThemeInitializationException {

        this.themeDir = themeDirPath;

        // load the theme elements and cache 'em
        loadThemeFromDisk();
    }

    /**
     * Get a resource representing the preview image for this theme.
     */
    @Override
    public ThemeResource getPreviewImage() {
        return this.previewImage;
    }

    /**
     * Get the collection of all templates associated with this Theme.
     */
    @Override
    public List<ThemeTemplate> getTemplates() {
        return new ArrayList<>(this.templatesByName.values());
    }

    /**
     * Lookup the stylesheet. Returns null if no stylesheet defined.
     */
    @Override
    public ThemeTemplate getStylesheet() {
        return this.stylesheet;
    }

    /**
     * Looup the default template, action = weblog. Returns null if the template
     * cannot be found.
     */
    @Override
    public ThemeTemplate getDefaultTemplate() {
        return this.templatesByAction.get(ComponentType.WEBLOG);
    }

    /**
     * Lookup the specified template by name. Returns null if the template
     * cannot be found.
     */
    @Override
    public ThemeTemplate getTemplateByName(String name) {
        return this.templatesByName.get(name);
    }

    /**
     * Lookup the specified template by link. Returns null if the template
     * cannot be found.
     */
    @Override
    public ThemeTemplate getTemplateByLink(String link) {
        return this.templatesByLink.get(link);
    }

    /**
     * Lookup the specified template by action. Returns null if the template
     * cannot be found.
     */
    @Override
    public ThemeTemplate getTemplateByAction(ComponentType action) {
        return this.templatesByAction.get(action);
    }

    /**
     * Get the collection of all resources associated with this Theme.
     * 
     * It is assured that the resources are returned sorted by pathname.
     */
    @Override
    public List<ThemeResource> getResources() {

        List<ThemeResource> myResources = new ArrayList<>(this.resources.values());
        // make sure resources are sorted.
        Collections.sort(myResources);

        return myResources;
    }

    /**
     * Lookup the specified resource by path. Returns null if the resource
     * cannot be found.
     */
    @Override
    public ThemeResource getResource(String path) {
        return this.resources.get(path);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(name);
        sb.append("\n");

        for (ThemeTemplate template : templatesByName.values()) {
            sb.append(template);
            sb.append("\n");
        }

        return sb.toString();
    }

    /**
     * Load all the elements of this theme from disk and cache them.
     */
    private void loadThemeFromDisk() throws ThemeInitializationException {

        log.debug("Parsing theme descriptor for " + this.themeDir);

        ThemeMetadata themeMetadata;
        try {
            // lookup theme descriptor and parse it
            ThemeMetadataParser parser = new ThemeMetadataParser();
            InputStream is = new FileInputStream(this.themeDir + File.separator
                    + "theme.xml");
            themeMetadata = parser.unmarshall(is);
        } catch (Exception ex) {
            throw new ThemeInitializationException(
                    "Unable to parse theme.xml for theme " + this.themeDir, ex);
        }

        log.debug("Loading Theme " + themeMetadata.getName());

        // use parsed theme descriptor to load Theme data
        setId(themeMetadata.getId());
        setName(themeMetadata.getName());
        if (StringUtils.isNotEmpty(themeMetadata.getDescription())) {
            setDescription(themeMetadata.getDescription());
        } else {
            setDescription(" ");
        }
        setAuthor(themeMetadata.getAuthor());
        setLastModified(null);
        setEnabled(true);

        // load resource representing preview image
        File previewFile = new File(this.themeDir + File.separator + themeMetadata.getPreviewImage());
        if (!previewFile.exists() || !previewFile.canRead()) {
            log.warn("Couldn't read theme [" + this.getName()
                    + "] preview image file ["
                    + themeMetadata.getPreviewImage() + "]");
        } else {
            this.previewImage = new SharedThemeResourceFromDir(
                    themeMetadata.getPreviewImage(), previewFile);
        }

        // available types with Roller
        List<RenditionType> availableTypesList = new ArrayList<>();
        availableTypesList.add(RenditionType.STANDARD);
        if (themeMetadata.getDualTheme()) {
            availableTypesList.add(RenditionType.MOBILE);
        }

        // load stylesheet if possible
        if (themeMetadata.getStylesheet() != null) {

            ThemeMetadataTemplate stylesheetTmpl = themeMetadata
                    .getStylesheet();
            // getting the template codes for available types
            ThemeMetadataTemplateRendition standardTemplateCode = stylesheetTmpl
                    .getTemplateRenditionTable().get(RenditionType.STANDARD);
            ThemeMetadataTemplateRendition mobileTemplateCode = stylesheetTmpl
                    .getTemplateRenditionTable().get(RenditionType.MOBILE);

            // standardTemplateCode required
            if (standardTemplateCode == null) {
                throw new ThemeInitializationException(
                        "Error in getting template codes for template");
            } else if (mobileTemplateCode == null && themeMetadata.getDualTheme()) {
                // clone the standard template code if no mobile is present
                mobileTemplateCode = new ThemeMetadataTemplateRendition();
                mobileTemplateCode.setContentsFile(standardTemplateCode
                        .getContentsFile());
                mobileTemplateCode.setTemplateLang(standardTemplateCode
                        .getTemplateLang());
                mobileTemplateCode.setType(RenditionType.MOBILE);

                stylesheetTmpl.addTemplateRendition(mobileTemplateCode);
            }

            // construct File object from path
            // we are getting the file path from standard as the default and
            // load it to initially.
            File templateFile = new File(this.themeDir + File.separator
                    + standardTemplateCode.getContentsFile());

            // read stylesheet contents
            String contents = loadTemplateFile(templateFile);
            if (contents == null) {
                // if we don't have any contents then skip this one
                log.error("Couldn't load stylesheet theme [" + this.getName()
                        + "] template file [" + templateFile + "]");
            } else {

                // construct ThemeTemplate representing this file
                // here we set content and template language from standard
                // template code assuming it is the default
                SharedThemeTemplate themeTemplate = new SharedThemeTemplate(
                        themeMetadata.getId() + ":"
                                + stylesheetTmpl.getName(),
                        stylesheetTmpl.getAction(), stylesheetTmpl.getName(),
                        stylesheetTmpl.getDescription(), contents,
                        stylesheetTmpl.getLink(), new Date(
                                templateFile.lastModified()), false, false);

                for (RenditionType type : availableTypesList) {
                    SharedThemeTemplateRendition rendition = createRendition(
                            themeTemplate.getId(),
                            stylesheetTmpl.getTemplateRendition(type));

                    themeTemplate.addTemplateRendition(rendition);

                    // Set Last Modified
                    Date lstModified = rendition.getLastModified();
                    if (getLastModified() == null
                            || lstModified.after(getLastModified())) {
                        setLastModified(lstModified);
                    }
                }
                // store it
                this.stylesheet = themeTemplate;

                // Update last modified
                themeTemplate.setLastModified(getLastModified());

                addTemplate(themeTemplate);
            }

        }

        // go through static resources and add them to the theme
        for (String resourcePath : themeMetadata.getResources()) {
            // construct ThemeResource object from resource
            File resourceFile = new File(this.themeDir + File.separator
                    + resourcePath);

            // Continue reading theme even if problem encountered with one file
            if (!resourceFile.exists() || !resourceFile.canRead()) {
                log.warn("Couldn't read  theme [" + this.getName()
                        + "] resource file [" + resourcePath + "]");
                continue;
            }

            // add it to the theme
            setResource(resourcePath, new SharedThemeResourceFromDir(
                    resourcePath, resourceFile));

            // Set Last Modified
            Date lstModified = new Date(resourceFile.lastModified());
            if (getLastModified() == null
                    || lstModified.after(getLastModified())) {
                setLastModified(lstModified);
            }

        }

        // go through templates and read in contents to a ThemeTemplate
        SharedThemeTemplate themeTemplate;
        for (ThemeMetadataTemplate templateMetadata : themeMetadata.getTemplates()) {

            // getting the template codes for available types
            ThemeMetadataTemplateRendition standardTemplateCode = templateMetadata
                    .getTemplateRenditionTable().get(RenditionType.STANDARD);
            ThemeMetadataTemplateRendition mobileTemplateCode = templateMetadata
                    .getTemplateRenditionTable().get(RenditionType.MOBILE);

            // If no template code present for any type
            if (standardTemplateCode == null) {
                throw new ThemeInitializationException(
                        "Error in getting template codes for template");
            } else if (mobileTemplateCode == null && themeMetadata.getDualTheme()) {
                // cloning the standard template code if no mobile is present
                mobileTemplateCode = new ThemeMetadataTemplateRendition();
                mobileTemplateCode.setContentsFile(standardTemplateCode
                        .getContentsFile());
                mobileTemplateCode.setTemplateLang(standardTemplateCode
                        .getTemplateLang());
                mobileTemplateCode.setType(RenditionType.MOBILE);

                templateMetadata.addTemplateRendition(mobileTemplateCode);
            }

            // construct File object from path
            File templateFile = new File(this.themeDir + File.separator
                    + standardTemplateCode.getContentsFile());

            String contents = loadTemplateFile(templateFile);
            if (contents == null) {
                // if we don't have any contents then skip this one
                throw new ThemeInitializationException("Couldn't load theme ["
                        + this.getName() + "] template file [" + templateFile
                        + "]");
            }

            // construct ThemeTemplate representing this file
            themeTemplate = new SharedThemeTemplate(
                    themeMetadata.getId() + ":" + templateMetadata.getName(),
                    templateMetadata.getAction(), templateMetadata.getName(),
                    templateMetadata.getDescription(), contents,
                    templateMetadata.getLink(), new Date(
                            templateFile.lastModified()),
                    templateMetadata.isHidden(), templateMetadata.isNavbar());

            for (RenditionType type : availableTypesList) {
                SharedThemeTemplateRendition templateCode = createRendition(
                        themeTemplate.getId(),
                        templateMetadata.getTemplateRendition(type));

                themeTemplate.addTemplateRendition(templateCode);

                // Set Last Modified
                Date lstModified = templateCode.getLastModified();
                if (getLastModified() == null
                        || lstModified.after(getLastModified())) {
                    setLastModified(lstModified);
                }
            }

            themeTemplate.setLastModified(getLastModified());

            // add it to the theme
            addTemplate(themeTemplate);

        }
    }

    /**
     * Load a single template file as a string, returns null if can't read file.
     */
    private String loadTemplateFile(File templateFile) {
        // Continue reading theme even if problem encountered with one file
        if (!templateFile.exists() && !templateFile.canRead()) {
            return null;
        }

        char[] chars;
        int length;
        try {
            chars = new char[(int) templateFile.length()];
            FileInputStream stream = new FileInputStream(templateFile);
            InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8);
            length = reader.read(chars);
        } catch (Exception noprob) {
            log.error("Exception reading theme [" + this.getName()
                    + "] template file [" + templateFile + "]");
            if (log.isDebugEnabled()) {
                log.debug(noprob);
            }
            return null;
        }

        return new String(chars, 0, length);
    }

    /**
     * Set the value for a given template name.
     */
    private void addTemplate(ThemeTemplate template) {
        this.templatesByName.put(template.getName(), template);
        this.templatesByLink.put(template.getLink(), template);
        if (!ComponentType.CUSTOM.equals(template.getAction())) {
            this.templatesByAction.put(template.getAction(), template);
        }
    }

    /**
     * Set the value for a given resource path.
     */
    private void setResource(String path, SharedThemeResourceFromDir resource) {
        // normalize to use web-style separators
        String normalizedPath = StringUtils.replace(path, "\\", "/");
        this.resources.put(normalizedPath, resource);
    }

    private SharedThemeTemplateRendition createRendition(String templateId,
            ThemeMetadataTemplateRendition templateCodeMetadata) {
        SharedThemeTemplateRendition templateRendition = new SharedThemeTemplateRendition();

        // construct File object from path
        File templateFile = new File(this.themeDir + File.separator
                + templateCodeMetadata.getContentsFile());

        // read stylesheet contents
        String contents = loadTemplateFile(templateFile);
        if (contents == null) {
            // if we don't have any contents then load no string
            contents = "";
            log.error("Couldn't load stylesheet theme [" + this.getName()
                    + "] template file [" + templateFile + "]");
        }
        //TODO: remove templateId above
        templateRendition.setTemplate(contents);
        templateRendition.setTemplateLanguage(templateCodeMetadata.getTemplateLang());
        templateRendition.setType(templateCodeMetadata.getType());
        templateRendition.setLastModified(new Date(templateFile.lastModified()));

        return templateRendition;
    }

}
