package tc.oc.commons.core.inspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Optional;
import java.util.stream.Stream;

import tc.oc.commons.core.stream.BiStream;
import tc.oc.commons.core.util.Chain;
import tc.oc.commons.core.util.Optionals;

public interface Inspectable {

    @Target({ElementType.FIELD, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @interface Inspect {
        /**
         * Override property name
         */
        String name() default  "";

        /**
         * Exclude nulls and empty {@link Optional}s
         */
        boolean optional() default true;

        /**
         * Expand arrays and collections
         */
        boolean deep() default true;

        /**
         * Only identify, don't inspect
         */
        boolean brief() default false;

        /**
         * Quote string/character values
         */
        boolean quote() default true;

        /**
         * Flatten child properties into parent
         */
        boolean inline() default false;
    }

    default String identify() {
        return Optionals.reduce(inspectType(), inspectIdentity(), (type, id) -> type + ":" + id);
    }

    default String inspect() {
        final TextInspector inspector = new TextInspector();
        return inspect(inspector, Inspection.defaults());
    }

    default <R> R inspect(Inspector<R> inspector, Inspection options) {
        return options.inspect(inspector, this, Chain.empty());
    }

    default String inspectType() {
        return getClass().getSimpleName();
    }

    default Optional<String> inspectIdentity() {
        return Optional.empty();
    }

    default <R> BiStream<String, R> inspectProperties(Inspector<R> inspector, Chain<Object> visited) {
        return BiStream.from(inspectableProperties().flatMap(property -> property.flatValues(this)))
                       .map((property, value) -> property.name(),
                            (property, value) -> property.options().inspect(inspector, value, visited.push(this)));
    }

    default Stream<? extends InspectableProperty> inspectableProperties() {
        return ReflectiveProperty.all(getClass()).stream();
    }

    class Impl implements Inspectable {
        @Override public String toString() {
            return inspect();
        }
    }
}
