/*
 * 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 java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.activation.FileTypeMap;
import javax.activation.MimetypesFileTypeMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.weblogger.WebloggerException;
import org.apache.roller.weblogger.business.InitializationException;
import org.apache.roller.weblogger.business.MediaFileManager;
import org.apache.roller.weblogger.business.WeblogManager;
import org.apache.roller.weblogger.business.Weblogger;
import org.apache.roller.weblogger.business.WebloggerFactory;
import org.apache.roller.weblogger.config.WebloggerConfig;
import org.apache.roller.weblogger.pojos.CustomTemplateRendition;
import org.apache.roller.weblogger.pojos.MediaFile;
import org.apache.roller.weblogger.pojos.MediaFileDirectory;
import org.apache.roller.weblogger.pojos.TemplateRendition;
import org.apache.roller.weblogger.pojos.TemplateRendition.RenditionType;
import org.apache.roller.weblogger.pojos.Theme;
import org.apache.roller.weblogger.pojos.ThemeResource;
import org.apache.roller.weblogger.pojos.ThemeTemplate;
import org.apache.roller.weblogger.pojos.ThemeTemplate.ComponentType;
import org.apache.roller.weblogger.pojos.Weblog;
import org.apache.roller.weblogger.pojos.WeblogTemplate;
import org.apache.roller.weblogger.pojos.WeblogTheme;
import org.apache.roller.weblogger.util.RollerMessages;

/**
 * Base implementation of a ThemeManager.
 * 
 * This particular implementation reads theme data off the filesystem and
 * assumes that those themes are not changeable at runtime.
 */
@com.google.inject.Singleton
public class ThemeManagerImpl implements ThemeManager {

	private static final FileTypeMap map;
	static {
		// TODO: figure out why PNG is missing from Java MIME types
		map = FileTypeMap.getDefaultFileTypeMap();
		if (map instanceof MimetypesFileTypeMap) {
			try {
				((MimetypesFileTypeMap) map).addMimeTypes("image/png png PNG");
			} catch (Exception ignored) {
			}
		}
	}

	private static final Log log = LogFactory.getLog(ThemeManagerImpl.class);
	private final Weblogger roller;
	// directory where themes are kept
	private String themeDir = null;
	// the Map contains ... (theme id, Theme)
	private Map<String, SharedTheme> themes = null;

	@com.google.inject.Inject
	protected ThemeManagerImpl(Weblogger roller) {

		this.roller = roller;

		// get theme directory from config and verify it
		this.themeDir = WebloggerConfig.getProperty("themes.dir");
		if (themeDir == null || themeDir.isBlank()) {
			throw new RuntimeException(
					"couldn't get themes directory from config");
		} else {
			// chop off trailing slash if it exists
			if (themeDir.endsWith("/")) {
				themeDir = themeDir.substring(0, themeDir.length() - 1);
			}

			// make sure it exists and is readable
			File themeDirFile = new File(themeDir);
			if (!themeDirFile.exists() || !themeDirFile.isDirectory()
					|| !themeDirFile.canRead()) {
				throw new RuntimeException("couldn't access theme dir ["
						+ themeDir + "]");
			}
		}
	}

    @Override
	public void initialize() throws InitializationException {

		log.debug("Initializing Theme Manager");

		if (themeDir != null) {
			// rather than be lazy we are going to load all themes from
			// the disk preemptive and cache them
			this.themes = loadAllThemesFromDisk();

			log.info("Successfully loaded " + this.themes.size() + " themes from disk.");
		}
	}

	/**
	 * @see org.apache.roller.weblogger.business.themes.ThemeManager#getTheme(java.lang.String)
	 */
    @Override
	public SharedTheme getTheme(String id) throws WebloggerException {

		// try to lookup theme from library
		SharedTheme theme = this.themes.get(id);

		// no theme? throw exception.
		if (theme == null) {
			throw new ThemeNotFoundException("Couldn't find theme [" + id + "]");
		}

		return theme;
	}

	/**
	 * @see org.apache.roller.weblogger.business.themes.ThemeManager#getTheme(Weblog)
	 */
    @Override
	public WeblogTheme getTheme(Weblog weblog) throws WebloggerException {

		if (weblog == null) {
			return null;
		}

		WeblogTheme weblogTheme = null;

		// if theme is custom or null then return a WeblogCustomTheme
		if (weblog.getEditorTheme() == null
				|| WeblogTheme.CUSTOM.equals(weblog.getEditorTheme())) {
			weblogTheme = new WeblogCustomTheme(weblog);

			// otherwise we are returning a WeblogSharedTheme
		} else {
			SharedTheme staticTheme = this.themes.get(weblog.getEditorTheme());
			if (staticTheme != null) {
				weblogTheme = new WeblogSharedTheme(weblog, staticTheme);
			} else {
				log.warn("Unable to lookup theme " + weblog.getEditorTheme());
			}
		}

		// TODO: if somehow the theme is still null should we provide some
		// kind of fallback option like a default theme?

		return weblogTheme;
	}

	/**
	 * @see org.apache.roller.weblogger.business.themes.ThemeManager#getEnabledThemesList()
	 */
    @Override
	public List<SharedTheme> getEnabledThemesList() {
		List<SharedTheme> allThemes = new ArrayList<>(this.themes.values());

		// sort 'em ... default ordering for themes is by name
		Collections.sort(allThemes);

		return allThemes;
	}

	/**
	 * @see org.apache.roller.weblogger.business.themes.ThemeManager#importTheme(Weblog,
	 *      SharedTheme, boolean)
	 */
    @Override
	public void importTheme(Weblog weblog, SharedTheme theme, boolean skipStylesheet)
			throws WebloggerException {

		log.debug("Importing theme [" + theme.getName() + "] to weblog ["
				+ weblog.getName() + "]");

		WeblogManager wmgr = roller.getWeblogManager();
		MediaFileManager fileMgr = roller.getMediaFileManager();

		MediaFileDirectory root = fileMgr.getDefaultMediaFileDirectory(weblog);
        if (root == null) {
            log.warn("Weblog " + weblog.getHandle() + " does not have a root MediaFile directory");
        }

		Set<ComponentType> importedActionTemplates = EnumSet.noneOf(ComponentType.class);
		ThemeTemplate stylesheetTemplate = theme.getStylesheet();
		for (ThemeTemplate themeTemplate : theme.getTemplates()) {
			WeblogTemplate template;

			// if template is an action, lookup by action
			if (themeTemplate.getAction() != null
					&& !themeTemplate.getAction().equals(ComponentType.CUSTOM)) {
				importedActionTemplates.add(themeTemplate.getAction());
				template = wmgr.getTemplateByAction(weblog,
                        themeTemplate.getAction());

				// otherwise, lookup by name
			} else {
				template = wmgr.getTemplateByName(weblog, themeTemplate.getName());
			}

			// Weblog does not have this template, so create it.
			if (template == null) {
				template = new WeblogTemplate();
				template.setWeblog(weblog);
			}

			// update template attributes except leave existing custom stylesheets as-is
			if (!themeTemplate.equals(stylesheetTemplate) || !skipStylesheet) {
				template.setAction(themeTemplate.getAction());
				template.setName(themeTemplate.getName());
				template.setDescription(themeTemplate.getDescription());
				template.setLink(themeTemplate.getLink());
				template.setHidden(themeTemplate.isHidden());
				template.setNavbar(themeTemplate.isNavbar());
                template.setOutputContentType(themeTemplate.getOutputContentType());
				template.setLastModified(new Date());

				// save it
				wmgr.saveTemplate(template);

                // create weblog template code objects and save them
                for (RenditionType type : RenditionType.values()) {

                    // See if we already have some code for this template already (eg previous theme)
                    CustomTemplateRendition weblogTemplateCode = template.getTemplateRendition(type);

                    // Get the template for the new theme
                    TemplateRendition templateCode = themeTemplate.getTemplateRendition(type);
                    if (templateCode != null) {

                        // Check for existing template
                        if (weblogTemplateCode == null) {
                            // Does not exist so create a new one
                            weblogTemplateCode = new CustomTemplateRendition(template, type);
                        }
                        weblogTemplateCode.setType(type);
                        weblogTemplateCode.setTemplate(templateCode.getTemplate());
                        weblogTemplateCode.setTemplateLanguage(templateCode
                                .getTemplateLanguage());
                        WebloggerFactory.getWeblogger().getWeblogManager()
                                .saveTemplateRendition(weblogTemplateCode);
                    }

                }
            }
		}

		// now, see if the weblog has left over non-custom action templates that
		// need to be deleted because they aren't in their new theme
        for (ComponentType action : ComponentType.values()) {
            if (action == ComponentType.CUSTOM) {
                continue;
            }
			// if we didn't import this action then see if it should be deleted
			if (!importedActionTemplates.contains(action)) {
				WeblogTemplate toDelete = wmgr.getTemplateByAction(weblog, action);
				if (toDelete != null) {
					log.debug("Removing stale action template " + toDelete.getId());
					wmgr.removeTemplate(toDelete);
				}
			}
		}

		// set weblog's theme to custom, then save
		weblog.setEditorTheme(WeblogTheme.CUSTOM);
		wmgr.saveWeblog(weblog);

		// now lets import all the theme resources
        for (ThemeResource resource : theme.getResources()) {

			log.debug("Importing resource " + resource.getPath());

			if (resource.isDirectory()) {
				MediaFileDirectory mdir = fileMgr.getMediaFileDirectoryByName(
						weblog, resource.getPath());
				if (mdir == null) {
					log.debug("    Creating directory: " + resource.getPath());
					fileMgr.createMediaFileDirectory(weblog, resource.getPath());
					roller.flush();
				} else {
					log.debug("    No action: directory already exists");
				}

			} else {
				String resourcePath = resource.getPath();

				MediaFileDirectory mdir;
				String justName;
				String justPath;

				if (resourcePath.indexOf('/') == -1) {
					mdir = fileMgr.getDefaultMediaFileDirectory(weblog);
					justPath = "";
					justName = resourcePath;

				} else {
					justPath = resourcePath.substring(0, resourcePath.lastIndexOf('/'));
					if (!justPath.startsWith("/")) {
                        justPath = "/" + justPath;
                    }
					justName = resourcePath.substring(resourcePath.lastIndexOf('/') + 1);
					mdir = fileMgr.getMediaFileDirectoryByName(weblog, justPath);
					if (mdir == null) {
						log.debug("    Creating directory: " + justPath);
						mdir = fileMgr.createMediaFileDirectory(weblog, justPath);
						roller.flush();
					}
				}

				MediaFile oldmf = fileMgr.getMediaFileByOriginalPath(weblog,
						justPath + "/" + justName);
				if (oldmf != null) {
					fileMgr.removeMediaFile(weblog, oldmf);
				}

				// save file without file-type, quota checks, etc.
				InputStream is = resource.getInputStream();
				MediaFile mf = new MediaFile();
				mf.setDirectory(mdir);
				mf.setWeblog(weblog);
				mf.setName(justName);
				mf.setOriginalPath(justPath + "/" + justName);
				mf.setContentType(map.getContentType(justName));
				mf.setInputStream(is);
				mf.setLength(resource.getLength());

				log.debug("    Saving file: " + justName);
				log.debug("    Saving in directory = " + mf.getDirectory());
				RollerMessages errors = new RollerMessages();
				fileMgr.createThemeMediaFile(weblog, mf, errors);
				try {
					is.close();
				} catch (IOException ex) {
					errors.addError("error.closingStream");
					log.debug("ERROR closing inputstream");
				}
				if (errors.getErrorCount() > 0) {
					throw new WebloggerException(errors.toString());
				}
				roller.flush();
			}
		}
	}

	/**
	 * This is a convenience method which loads all the theme data from themes
	 * stored on the filesystem in the roller webapp /themes/ directory.
	 */
	private Map<String, SharedTheme> loadAllThemesFromDisk() {

		Map<String, SharedTheme> themeMap = new HashMap<>();

		// first, get a list of the themes available
		File themesdir = new File(this.themeDir);
		FilenameFilter filter = (File dir, String name) -> {
            File file = new File(dir.getAbsolutePath() + File.separator + name);
            return file.isDirectory() && !file.getName().startsWith(".");
        };
		String[] themenames = themesdir.list(filter);

		if (themenames == null) {
			log.warn("No themes found!  Perhaps wrong directory for themes specified?  "
					+ "(Check themes.dir setting in roller[-custom].properties file.)");
		} else {
            log.info("Loading themes from " + themesdir.getAbsolutePath() + "...");

            // now go through each theme and load it into a Theme object
            for (String themeName : themenames) {
                try {
                    SharedTheme theme = new SharedThemeFromDir(this.themeDir + File.separator + themeName);
                    themeMap.put(theme.getId(), theme);
                    log.info("Loaded theme '" + themeName + "'");
                } catch (Exception unexpected) {
                    // shouldn't happen, so let's learn why it did
                    log.error("Problem processing theme '" + themeName + "':", unexpected);
                }
            }
        }

		return themeMap;
	}

	/**
	 * @see ThemeManager#reLoadThemeFromDisk(String)
	 */
    @Override
	public boolean reLoadThemeFromDisk(String reloadTheme) {

		boolean reloaded = false;

		try {

            SharedTheme theme = new SharedThemeFromDir(this.themeDir + File.separator
					+ reloadTheme);

            Theme loadedTheme = themes.get(theme.getId());

            if (loadedTheme != null
                    && theme.getLastModified().after(
                            loadedTheme.getLastModified())) {
                themes.remove(theme.getId());
                themes.put(theme.getId(), theme);
                reloaded = true;
            }

		} catch (Exception unexpected) {
			// shouldn't happen, so let's learn why it did
			log.error("Problem reloading theme " + reloadTheme, unexpected);
		}

		return reloaded;

	}
}
