/*
 *  Copyright (c) 2011-2017 Contributors to the Eclipse Foundation
 *
 *  See the NOTICE file(s) distributed with this work for additional
 *  information regarding copyright ownership.
 *
 *  Licensed 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.
 *
 *  Contributors:
 */
package config.seimpl;

import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.SortedSet;
import java.util.concurrent.ConcurrentSkipListSet;

import javax.enterprise.inject.Vetoed;

import config.seimpl.converters.StringArrayConverter;
import org.eclipse.microprofile.config.Config;
import config.seimpl.converters.BooleanConverter;
import config.seimpl.converters.ByteConverter;
import config.seimpl.converters.CharacterConverter;
import config.seimpl.converters.DoubleConverter;
import config.seimpl.converters.DurationConverter;
import config.seimpl.converters.FloatConverter;
import config.seimpl.converters.IntegerConverter;
import config.seimpl.converters.LocalDateTimeConverter;
import config.seimpl.converters.LongConverter;
import config.seimpl.converters.ShortConverter;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.Converter;

/**
 * Created by starksm on 6/1/17.
 */
@Vetoed
public class DefaultConfig implements Config {
    private static SystemEnvConfigSource systemEnvCS = new SystemEnvConfigSource();
    private static SystemPropertyConfigSource systemPropertyCS = new SystemPropertyConfigSource();

    /**
     * The list of ConfigSource objects ordered by their ordinal value
     */
    private SortedSet<ConfigSource> sources = new ConcurrentSkipListSet<>(Comparator.comparing(ConfigSource::getOrdinal));
    private HashMap<Type, Converter> converters = new HashMap<>();

    DefaultConfig() {
        loadStandardConverters();
    }
    DefaultConfig(List<ConfigSource> sources) {
        this.sources.addAll(sources);
        loadStandardConverters();
    }

    public void loadStandardSources(ClassLoader loader) {
        if(loader == null) {
            loader = getClass().getClassLoader();
        }
        sources.add(systemPropertyCS);
        System.out.printf("Added System property ConfigSource\n");
        sources.add(systemEnvCS);
        System.out.printf("Added System environment ConfigSource\n");
        try {
            sources.add(new DefaultMPConfigSource(loader));
            System.out.printf("Added META-INF/microprofile-config.properties ConfigSource\n");
        }
        catch (IOException e) {
            // Ignore
        }
    }

    public void loadStandardConverters() {
        converters.put(Boolean.class, new BooleanConverter());
        converters.put(boolean.class, new BooleanConverter());
        converters.put(Character.class, new CharacterConverter());
        converters.put(char.class, new CharacterConverter());
        converters.put(Byte.class, new ByteConverter());
        converters.put(byte.class, new ByteConverter());
        converters.put(Short.class, new ShortConverter());
        converters.put(short.class, new ShortConverter());
        converters.put(Integer.class, new IntegerConverter());
        converters.put(int.class, new IntegerConverter());
        converters.put(Long.class, new LongConverter());
        converters.put(long.class, new LongConverter());
        converters.put(Float.class, new FloatConverter());
        converters.put(float.class, new FloatConverter());
        converters.put(Double.class, new DoubleConverter());
        converters.put(double.class, new DoubleConverter());
        converters.put(Duration.class, new DurationConverter());
        converters.put(LocalDateTime.class, new LocalDateTimeConverter());
        converters.put(String[].class, new StringArrayConverter());
    }

    /**
     * Use the {@link Converter}s registered with the config to convert a String value to a target property type.
     * @param svalue - the string value representation of the property
     * @param propertyType - the desired Java type of the property
     * @return the converted value
     * @throws TypeNotPresentException if there is no registered Converter
     */
    public <T> T convertValue(String svalue, Class<T> propertyType) {
        T value = null;
        if(propertyType.isAssignableFrom(String.class)) {
            value = propertyType.cast(svalue);
        }
        else {
            Converter<T> converter = converters.get(propertyType);
            if(converter != null) {
                value = converter.convert(svalue);
            }
            else {
                System.err.printf("Failed to find Converter for type: %s\n", propertyType);
                throw new TypeNotPresentException(propertyType.getTypeName(), null);
            }
        }
        return value;
    }

    /**
     * Use the {@link Converter}s registered with the config to try to convert a String value to a target property type.
     * @param svalue - the string value representation of the property
     * @param propertyType - the desired Java type of the property
     * @return the converted value if a matching converter is found, Optional.empty() otherwise
     */
    public <T> Optional<T> tryConvertValue(String svalue, Class<T> propertyType) {
        Optional<T> value = Optional.empty();
        if(propertyType.isAssignableFrom(String.class)) {
            value = Optional.of((T)svalue);
        }
        else {
            Converter<T> converter = converters.get(propertyType);
            if(converter != null) {
                value = Optional.of(converter.convert(svalue));
            }
            else {
                System.err.printf("Failed to find Converter for type: %s\n", propertyType);
            }
        }
        return value;
    }

    /**
     * Add a new ConfigSource. This will be added to the existing sources based on the {@link ConfigSource#getOrdinal()} value.
     * @param cs the ConfigSource to add
     * @return true if the ConfigSource was added, false otherwise
     */
    public boolean addConfigSource(ConfigSource cs) {
        return sources.add(cs);
    }
    public void addConverter(Converter converter) {
        // Determine the target type of the converter
        Type[] genericInterfaces = converter.getClass().getGenericInterfaces();
        for(Type type : genericInterfaces) {
            if(type instanceof ParameterizedType) {
                ParameterizedType ptype = (ParameterizedType) type;
                if(ptype.getRawType().equals(Converter.class)) {
                    Type actualType = ptype.getActualTypeArguments()[0];
                    converters.put(actualType, converter);
                    System.out.printf("+++ Added converter(%s) for type: %s\n", converter, actualType);
                }
            }
        }
    }

    @Override
    public <T> T getValue(String propertyName, Class<T> propertyType) {
        T value = getOptionalValue(propertyName, propertyType).orElse(null);
        return value;
    }

    @Override
    public <T> Optional<T> getOptionalValue(String propertyName, Class<T> propertyType) {
        Optional<T> value = Optional.empty();
        for (ConfigSource cs : sources) {
            String svalue = cs.getValue(propertyName);
            if(svalue == null) {
                value = Optional.empty();
            }
            else if(propertyType.isAssignableFrom(String.class)) {
                value = Optional.of(propertyType.cast(svalue));
                break;
            }
            else {
                Converter<T> converter = converters.get(propertyType);
                if(converter != null) {
                    value = Optional.of(converter.convert(svalue));
                }
                else {
                    System.err.printf("Failed to find Converter for: %s of type: %s\n", propertyName, propertyType);
                }
                break;
            }
        }
        return value;
    }

    @Override
    public Iterable<String> getPropertyNames() {
        return null;
    }

    @Override
    public Iterable<ConfigSource> getConfigSources() {
        return sources;
    }
}
