/*
 * 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.sshd.common;

import java.nio.charset.Charset;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;

import org.apache.sshd.common.util.ValidateUtils;

/**
 * Property definition.
 *
 * @param  <T> The generic property type
 * @author     <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public interface Property<T> extends NamedResource {

    static Property<String> string(String name) {
        return string(name, null);
    }

    static Property<String> string(String name, String def) {
        return new StringProperty(name, def);
    }

    static Property<Boolean> bool(String name) {
        return new BooleanProperty(name);
    }

    static Property<Boolean> bool(String name, boolean def) {
        return new BooleanProperty(name, def);
    }

    static Property<Integer> integer(String name) {
        return new IntegerProperty(name);
    }

    static Property<Integer> integer(String name, int def) {
        return new IntegerProperty(name, def);
    }

    // CHECKSTYLE:OFF
    static Property<Long> long_(String name) {
        return new LongProperty(name);
    }

    static Property<Long> long_(String name, long def) {
        return new LongProperty(name, def);
    }

    static <T extends Enum<T>> Property<T> enum_(String name, Class<T> type) {
        return enum_(name, type, null);
    }

    static <T extends Enum<T>> Property<T> enum_(String name, Class<T> type, T def) {
        return new EnumProperty<>(name, type, def);
    }
    // CHECKSTYLE:ON

    static Property<Duration> duration(String name) {
        return duration(name, null);
    }

    static Property<Duration> duration(String name, Duration def) {
        return new DurationProperty(name, def);
    }

    static Property<Duration> duration(String name, Duration def, Duration min) {
        return new DurationProperty(name, def, min);
    }

    static Property<Duration> durationSec(String name) {
        return durationSec(name, null);
    }

    static Property<Duration> durationSec(String name, Duration def) {
        return new DurationInSecondsProperty(name, def);
    }

    static Property<Duration> durationSec(String name, Duration def, Duration min) {
        return new DurationInSecondsProperty(name, def, min);
    }

    static Property<Charset> charset(String name) {
        return charset(name, null);
    }

    static Property<Charset> charset(String name, Charset def) {
        return new CharsetProperty(name, def);
    }

    static Property<Object> object(String name) {
        return object(name, null);
    }

    static Property<Object> object(String name, Object def) {
        return new ObjectProperty(name, def);
    }

    static <T> Property<T> validating(Property<T> prop, Consumer<? super T> validator) {
        return new Validating<>(prop, validator);
    }

    abstract class BaseProperty<T> implements Property<T> {
        private final String name;
        private final Class<T> type;
        private final Optional<T> defaultValue;

        protected BaseProperty(String name, Class<T> type) {
            this(name, type, null);
        }

        protected BaseProperty(String name, Class<T> type, T defaultValue) {
            this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No name provided");
            this.type = Objects.requireNonNull(type, "Type must be provided");
            this.defaultValue = Optional.ofNullable(defaultValue);
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public Class<T> getType() {
            return type;
        }

        @Override
        public Optional<T> getDefault() {
            return defaultValue;
        }

        @Override
        public Optional<T> get(PropertyResolver resolver) {
            Object propValue = PropertyResolverUtils.resolvePropertyValue(resolver, getName());
            return (propValue != null) ? Optional.of(fromStorage(propValue)) : getDefault();
        }

        @Override
        public T getOrCustomDefault(PropertyResolver resolver, T defaultValue) {
            Object propValue = PropertyResolverUtils.resolvePropertyValue(resolver, getName());
            return (propValue != null) ? fromStorage(propValue) : defaultValue;
        }

        @Override
        public void set(PropertyResolver resolver, T value) {
            PropertyResolverUtils.updateProperty(resolver, getName(), toStorage(value));
        }

        protected Object toStorage(T value) {
            return value;
        }

        protected abstract T fromStorage(Object value);

        @Override
        public String toString() {
            return "Property[" + getName() + "](" + getType().getSimpleName() + "]";
        }
    }

    class DurationProperty extends BaseProperty<Duration> {

        protected final Duration min;

        public DurationProperty(String name) {
            this(name, null);
        }

        public DurationProperty(String name, Duration def) {
            super(name, Duration.class, def);
            min = null;
        }

        public DurationProperty(String name, Duration def, Duration min) {
            super(name, Duration.class, atLeast(name, def, min));
            this.min = min;
        }

        @Override
        protected Object toStorage(Duration value) {
            atLeast(getName(), value, min);
            return (value != null) ? value.toMillis() : null;
        }

        @Override
        protected Duration fromStorage(Object value) {
            Long val = PropertyResolverUtils.toLong(value);
            return (val != null) ? Duration.ofMillis(val) : null;
        }

        private static Long toMillis(Duration value) {
            return value == null ? null : value.toMillis();
        }

        protected static Duration atLeast(String name, Duration value, Duration min) {
            if (min != null) {
                ValidateUtils.checkTrue(value != null && min.compareTo(value) <= 0,
                        "Property %s must be at least %d ms; actual value: %d ms", name, toMillis(min), toMillis(value));
            }
            return value;
        }
    }

    class DurationInSecondsProperty extends DurationProperty {
        public DurationInSecondsProperty(String name) {
            this(name, null);
        }

        public DurationInSecondsProperty(String name, Duration def) {
            super(name, def);
        }

        public DurationInSecondsProperty(String name, Duration def, Duration min) {
            super(name, def, min);
        }

        @Override
        protected Object toStorage(Duration value) {
            atLeast(getName(), value, min);
            return (value != null) ? value.getSeconds() : null;
        }

        @Override
        protected Duration fromStorage(Object value) {
            Long val = PropertyResolverUtils.toLong(value);
            return val != null ? Duration.ofSeconds(val) : null;
        }
    }

    class StringProperty extends BaseProperty<String> {
        public StringProperty(String name) {
            this(name, null);
        }

        public StringProperty(String name, String def) {
            super(name, String.class, def);
        }

        @Override
        protected String fromStorage(Object value) {
            return (value != null) ? value.toString() : null;
        }
    }

    class BooleanProperty extends BaseProperty<Boolean> {
        public BooleanProperty(String name) {
            this(name, null);
        }

        public BooleanProperty(String name, Boolean defaultValue) {
            super(name, Boolean.class, defaultValue);
        }

        @Override
        protected Boolean fromStorage(Object value) {
            return PropertyResolverUtils.toBoolean(value);
        }
    }

    class LongProperty extends BaseProperty<Long> {
        public LongProperty(String name) {
            this(name, null);
        }

        public LongProperty(String name, Long defaultValue) {
            super(name, Long.class, defaultValue);
        }

        @Override
        protected Long fromStorage(Object value) {
            return PropertyResolverUtils.toLong(value);
        }
    }

    class IntegerProperty extends BaseProperty<Integer> {
        public IntegerProperty(String name) {
            this(name, null);
        }

        public IntegerProperty(String name, Integer defaultValue) {
            super(name, Integer.class, defaultValue);
        }

        @Override
        protected Integer fromStorage(Object value) {
            return PropertyResolverUtils.toInteger(value);
        }
    }

    class CharsetProperty extends BaseProperty<Charset> {
        public CharsetProperty(String name) {
            this(name, null);
        }

        public CharsetProperty(String name, Charset defaultValue) {
            super(name, Charset.class, defaultValue);
        }

        @Override
        protected Charset fromStorage(Object value) {
            return PropertyResolverUtils.toCharset(value);
        }
    }

    class ObjectProperty extends BaseProperty<Object> {
        public ObjectProperty(String name) {
            this(name, null);
        }

        public ObjectProperty(String name, Object defaultValue) {
            super(name, Object.class, defaultValue);
        }

        @Override
        protected Object fromStorage(Object value) {
            return value;
        }
    }

    class EnumProperty<T extends Enum<T>> extends BaseProperty<T> {
        protected final Collection<T> values;

        public EnumProperty(String name, Class<T> type) {
            this(name, type, null);
        }

        public EnumProperty(String name, Class<T> type, T def) {
            super(name, type, def);
            values = Collections.unmodifiableSet(EnumSet.allOf(type));
        }

        @Override
        protected T fromStorage(Object value) {
            Class<T> type = getType();
            return PropertyResolverUtils.toEnum(type, value, false, values);
        }
    }

    class Validating<T> implements Property<T> {
        protected final Property<T> delegate;
        protected final Consumer<? super T> validator;

        public Validating(Property<T> delegate, Consumer<? super T> validator) {
            this.delegate = delegate;
            this.validator = validator;
        }

        @Override
        public String getName() {
            return delegate.getName();
        }

        @Override
        public Class<T> getType() {
            return delegate.getType();
        }

        @Override
        public Optional<T> getDefault() {
            return delegate.getDefault();
        }

        @Override
        public T getRequiredDefault() {
            return delegate.getRequiredDefault();
        }

        @Override
        public Optional<T> get(PropertyResolver resolver) {
            Optional<T> t = delegate.get(resolver);
            t.ifPresent(validator);
            return t;
        }

        @Override
        public T getOrCustomDefault(PropertyResolver resolver, T defaultValue) {
            T value = delegate.getOrCustomDefault(resolver, defaultValue);
            validator.accept(value);
            return value;
        }

        @Override
        public void set(PropertyResolver resolver, T value) {
            validator.accept(value);
            delegate.set(resolver, value);
        }

        @Override
        public void remove(PropertyResolver resolver) {
            delegate.remove(resolver);
        }
    }

    /**
     * @return Property type - <B>Note:</B> for primitive types the wrapper equivalent is returned
     */
    Class<T> getType();

    /**
     * @return The {@link Optional} pre-defined default value
     */
    Optional<T> getDefault();

    default T getRequiredDefault() {
        return getDefault().get();
    }

    /**
     * @param  resolver The {@link PropertyResolver} to query for the property value.
     * @return          The {@link Optional} result - if resolver contains a value then the resolver's value, otherwise
     *                  the pre-defined {@link #getDefault() default}
     */
    Optional<T> get(PropertyResolver resolver);

    /**
     * @param  resolver               The {@link PropertyResolver} to query for the property value.
     * @return                        The resolved value
     * @throws NoSuchElementException if resolver contains no value and no {@link #getDefault()} defined
     */
    default T getRequired(PropertyResolver resolver) {
        return get(resolver).get();
    }

    /**
     * @param  resolver The {@link PropertyResolver} to query for the property value.
     * @return          The resolver's value or {@code null} if no specific value found in the resolver - regardless of
     *                  whether there is a default value
     */
    default T getOrNull(PropertyResolver resolver) {
        return getOrCustomDefault(resolver, null);
    }

    /**
     * @param  resolver     The {@link PropertyResolver} to query for the property value.
     * @param  defaultValue The default value to return if no specific value found in resolver
     * @return              The resolver's value or specified default if no specific value found in the resolver -
     *                      regardless of whether there is a default value
     */
    T getOrCustomDefault(PropertyResolver resolver, T defaultValue);

    /**
     * @param resolver The {@link PropertyResolver} to update with the property value.
     * @param value    The value to set
     */
    void set(PropertyResolver resolver, T value);

    /**
     * @param resolver The {@link PropertyResolver} to remove the property from
     */
    default void remove(PropertyResolver resolver) {
        PropertyResolverUtils.updateProperty(resolver, getName(), null);
    }
}
