/*
 * 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.karaf.config.core.impl;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;

import org.apache.felix.cm.json.Configurations;
import org.apache.felix.utils.properties.TypedProperties;
import org.apache.karaf.config.core.ConfigRepository;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ConfigRepositoryImpl implements ConfigRepository {

    private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRepositoryImpl.class);

    private static final String FILEINSTALL_FILE_NAME = "felix.fileinstall.filename";

    private ConfigurationAdmin configAdmin;

    public ConfigRepositoryImpl(ConfigurationAdmin configAdmin) {
        this.configAdmin = configAdmin;
    }

    @Override
    public void update(String pid, Map<String, Object> properties) throws IOException {
        update(pid, properties, "cfg");
    }

    /* (non-Javadoc)
     * @see org.apache.karaf.shell.config.impl.ConfigRepository#update(java.lang.String, java.util.Dictionary, boolean)
     */
    @Override
    public void update(String pid, Map<String, Object> properties, String suffix) throws IOException {
        try {
            LOGGER.trace("Updating configuration {}", pid);
            Configuration cfg = configAdmin.getConfiguration(pid, "?");
            Dictionary<String, Object> dict = cfg.getProcessedProperties(null);
            TypedProperties props = new TypedProperties();
            File file = getCfgFileFromProperties(dict);
            if (file != null) {
                props = load(file);
                props.putAll(properties);
                props.keySet().retainAll(properties.keySet());
                store(props, file);
                props.clear();
                props = load(file);
                props.put(FILEINSTALL_FILE_NAME, file.toURI().toString());
            } else {
                if (properties.containsKey(FILEINSTALL_FILE_NAME)) {
                    file = getCfgFileFromProperty(properties.get(FILEINSTALL_FILE_NAME));
                }
                if (file == null) {
                    file = generateConfigFilename(cfg, suffix);
                }
                props.putAll(properties);
                props.keySet().retainAll(properties.keySet());
                store(props, file);
                props.put(FILEINSTALL_FILE_NAME, file.toURI().toString());
            }
            cfg.update(new Hashtable<>(props));
        } catch (URISyntaxException e) {
            throw new IOException("Error updating config", e);
        }
    }
    
    private static File generateConfigFilename(Configuration cfg, String suffix) {
        final String pid = cfg.getPid();
        final String factoryPid = cfg.getFactoryPid();
        String fName;
        if(factoryPid!=null) {
            //pid = <factoryPid>.<identifier>
            String identifier = pid.substring(factoryPid.length()+1);
            fName = cfg.getFactoryPid() + "-"+identifier+ "."  + suffix;
        }
        else {
            fName = pid + "."  + suffix;
        }
        return new File(System.getProperty("karaf.etc"), fName);
    }

    /* (non-Javadoc)
     * @see org.apache.karaf.shell.config.impl.ConfigRepository#delete(java.lang.String)
     */
    @Override
    public void delete(String pid) throws Exception {
        LOGGER.trace("Deleting configuration {}", pid);
        Configuration configuration = configAdmin.getConfiguration(pid, null);
        configuration.delete();
    }

    @Override
    public boolean exists(String pid) throws Exception {
        Configuration[] configurations = configAdmin.listConfigurations("(service.pid=" + pid + ")");
        if (configurations == null || configurations.length == 0) {
            return false;
        }
        return true;
    }

    private File getCfgFileFromProperties(Dictionary<String, Object> properties) throws URISyntaxException, MalformedURLException {
        if (properties != null) {
            Object val = properties.get(FILEINSTALL_FILE_NAME);
            return getCfgFileFromProperty(val);
        }
        return null;
    }

    private File getCfgFileFromProperty(Object val) throws URISyntaxException, MalformedURLException {
        if (val instanceof URL) {
            return new File(((URL) val).toURI());
        }
        if (val instanceof URI) {
            return new File((URI) val);
        }
        if (val instanceof String) {
            return new File(new URL((String) val).toURI());
        }
        return null;
    }

    @Override
    public TypedProperties getConfig(String pid) throws IOException, InvalidSyntaxException {
        if (pid != null && configAdmin != null) {
            Configuration configuration = configAdmin.getConfiguration(pid, null);
            if (configuration != null) {
                TypedProperties tp = new TypedProperties();
                Dictionary<String, Object> props = configuration.getProcessedProperties(null);
                if (props != null) {
                    File file;
                    try {
                        file = getCfgFileFromProperties(props);
                    } catch (URISyntaxException e) {
                        throw new IOException(e);
                    }
                    if (file != null && file.exists()) {
                        tp = load(file);

                        Object fileInstallFileName = props.get(FILEINSTALL_FILE_NAME);
                        if (fileInstallFileName != null && !"".equals(fileInstallFileName)) {
                            tp.put(FILEINSTALL_FILE_NAME, fileInstallFileName);
                        }

                    } else {
                        for (Enumeration<String> e = props.keys(); e.hasMoreElements();) {
                            String key = e.nextElement();
                            Object val = props.get(key);
                            tp.put(key, val);
                        }
                        tp.remove(Constants.SERVICE_PID);
                        tp.remove(ConfigurationAdmin.SERVICE_FACTORYPID);
                    }
                }
                return tp;
            }
        }
        return null;
    }

    @Override
    public String createFactoryConfiguration(String factoryPid, Map<String, Object> properties) throws IOException {
        return createFactoryConfiguration(factoryPid, null, properties, "cfg");
    }

    @Override
    public String createFactoryConfiguration(String factoryPid, Map<String, Object> properties, String suffix) throws IOException {
        return createFactoryConfiguration(factoryPid, null, properties, suffix);
    }

    @Override
    public String createFactoryConfiguration(String factoryPid, String alias, Map<String, Object> properties) throws IOException {
        return createFactoryConfiguration(factoryPid, alias, properties, "cfg");
    }

    @Override
    public String createFactoryConfiguration(String factoryPid, String alias, Map<String, Object> properties, String suffix) throws IOException {
        Configuration config = configAdmin.createFactoryConfiguration(factoryPid, "?");
        TypedProperties props = new TypedProperties();
        File file = null;
        if (alias != null && !"".equals(alias.trim())) {
            file = new File(new File(System.getProperty("karaf.etc")), factoryPid + "-" + alias + "." + suffix);
        } else {
            file = Files.createTempFile(new File(System.getProperty("karaf.etc")).toPath(), factoryPid + "-", "." + suffix).toFile();
        }
        props.putAll(properties);
        store(props, file);
        props.put(FILEINSTALL_FILE_NAME, file.toURI().toString());
        config.update(new Hashtable<>(props));
        return config.getPid();
    }

    @Override
    public ConfigurationAdmin getConfigAdmin() {
        return configAdmin;
    }

    static TypedProperties load(File file) throws IOException {
        TypedProperties props = new TypedProperties();
        if (file.toURI().toString().endsWith(".json")) {
            Hashtable<String, Object> configuration = Configurations.buildReader().build(new FileReader(file)).readConfiguration();
            for (String key : configuration.keySet()) {
                props.put(key, configuration.get(key));
            }
        } else {
            props.load(file);
        }
        return props;
    }

    void store(TypedProperties properties, File file) throws IOException {
        if (file.toURI().toString().endsWith(".json")) {
            Configurations.buildWriter().build(new FileWriter(file)).writeConfiguration(new Hashtable<>(properties));
        } else {
            properties.save(file);
        }
    }

}
