/*
 * Copyright 2017 HugeGraph Authors
 *
 * 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 com.baidu.hugegraph.config;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;

import org.apache.commons.configuration.PropertyConverter;
import org.slf4j.Logger;

import com.baidu.hugegraph.util.E;
import com.baidu.hugegraph.util.Log;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;

public class TypedOption<T, R> {

    private static final Logger LOG = Log.logger(TypedOption.class);

    private static final Set<Class<?>> ACCEPTED_DATA_TYPES;
    private static final String ACCEPTED_DATA_TYPES_STRING;

    static {
        ACCEPTED_DATA_TYPES = ImmutableSet.of(
                Boolean.class,
                Short.class,
                Integer.class,
                Byte.class,
                Long.class,
                Float.class,
                Double.class,
                String.class,
                String[].class,
                List.class
        );

        ACCEPTED_DATA_TYPES_STRING = Joiner.on(", ").join(ACCEPTED_DATA_TYPES);
    }

    private final String name;
    private final String desc;
    private final boolean required;
    private final Class<T> dataType;
    private final T defaultValue;
    private final Predicate<T> checkFunc;

    @SuppressWarnings("unchecked")
    public TypedOption(String name, boolean required, String desc,
                       Predicate<T> pred, Class<T> type, T value) {
        E.checkNotNull(name, "name");
        E.checkNotNull(type, "dataType");

        this.name = name;
        this.dataType = (Class<T>) this.checkAndAssignDataType(type);
        this.defaultValue = value;
        this.required = required;
        this.desc = desc;
        this.checkFunc = pred;

        this.check(this.defaultValue);
    }

    private Class<?> checkAndAssignDataType(Class<T> dataType) {
        for (Class<?> clazz : ACCEPTED_DATA_TYPES) {
            if (clazz.isAssignableFrom(dataType)) {
                return clazz;
            }
        }

        String msg = String.format("Input data type '%s' doesn't belong " +
                                   "to acceptable type set: [%s]",
                                   dataType, ACCEPTED_DATA_TYPES_STRING);
        throw new IllegalArgumentException(msg);
    }

    public String name() {
        return this.name;
    }

    public Class<T> dataType() {
        return this.dataType;
    }

    public String desc() {
        return this.desc;
    }

    public boolean required() {
        return this.required;
    }

    public R defaultValue() {
        return this.convert(this.defaultValue);
    }

    public R parseConvert(Object value) {
        T parsed = this.parse(value);
        this.check(parsed);
        return this.convert(parsed);
    }

    @SuppressWarnings("unchecked")
    protected T parse(Object value) {
        return (T) this.parse(value, this.dataType);
    }

    protected Object parse(Object value, Class<?> dataType) {
        if (dataType.equals(String.class)) {
            return value;
        } else if (List.class.isAssignableFrom(dataType)) {
            E.checkState(this.forList(),
                         "List option can't be registered with class %s",
                         this.getClass().getSimpleName());
        }

        // Use PropertyConverter method `toXXX` convert value
        String methodTo = "to" + dataType.getSimpleName();
        try {
            Method method = PropertyConverter.class.getMethod(
                            methodTo, Object.class);
            return method.invoke(null, value);
        } catch (ReflectiveOperationException e) {
            LOG.error("Invalid type of value '{}' for option '{}'",
                      value, this.name, e);
            throw new ConfigException(
                      "Invalid type of value '%s' for option '%s', " +
                      "expect '%s' type",
                      value, this.name, dataType.getSimpleName());
        }
    }

    protected void check(Object value) {
        E.checkNotNull(value, "value", this.name);
        if (!this.dataType.isInstance(value)) {
            throw new ConfigException(
                      "Invalid type of value '%s' for option '%s', " +
                      "expect type %s but got %s", value, this.name,
                      this.dataType.getSimpleName(),
                      value.getClass().getSimpleName());
        }

        if (this.checkFunc != null) {
            @SuppressWarnings("unchecked")
            T result = (T) value;
            if (!this.checkFunc.apply(result)) {
                throw new ConfigException("Invalid option value for '%s': %s",
                                          this.name, value);
            }
        }
    }

    @SuppressWarnings("unchecked")
    protected R convert(T value) {
        return (R) value;
    }

    protected boolean forList() {
        return false;
    }

    @Override
    public String toString() {
        return String.format("[%s]%s=%s", this.dataType.getSimpleName(),
                             this.name, this.defaultValue);
    }
}