package org.implab.gradle.common.tasks;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import javax.inject.Inject;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFile;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Provider;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import org.implab.gradle.common.json.MapImportSpec;
import org.implab.gradle.common.json.Json;
import org.implab.gradle.common.json.JsonObjectSpec;
import org.implab.gradle.common.utils.Closures;
import org.implab.gradle.common.utils.Properties;

import groovy.lang.Closure;

/**
 * A Gradle task that writes JSON content to a file.
 *
 * This task allows you to build up a JSON object through various methods and
 * write it to a file.
 * You can add content directly, import from other JSON files, or merge in map
 * data with optional filtering.
 *
 * <h3>Usage Example:</h3>
 *
 * <pre>
 * tasks.register('generateConfig', WriteJson) {
 *     outputFile = file('build/config.json')
 *     content {
 *         version = '1.0.0'
 *         name = 'myapp'
 *     }
 *     from file('src/base-config.json')
 *     putAll providers.provider { ['debug': true] }
 * }
 * </pre>
 *
 * <h3>Properties:</h3>
 * <ul>
 * <li><strong>content</strong> - A map of string keys to object values that
 * will be serialized to JSON</li>
 * <li><strong>outputFile</strong> - The file where the JSON will be
 * written</li>
 * </ul>
 *
 * <h3>Methods:</h3>
 * <ul>
 * <li><strong>content()</strong> - Configure content using a closure or
 * action</li>
 * <li><strong>from()</strong> - Import JSON content from a file with optional
 * key filtering</li>
 * <li><strong>putAll()</strong> - Merge a map provider with optional key
 * filtering</li>
 * </ul>
 *
 * @since 1.0
 */
@NonNullByDefault
public abstract class WriteJson extends DefaultTask {

    private final ProviderFactory providers;

    private final JsonObjectSpec contentSpec = new JsonObjectSpec() {
        @Override
        public void set(String key, Object value) {
            Properties.putMapEntry(getContent(), key, value);
        }
    };

    @Input
    @Optional
    public abstract MapProperty<String, Object> getContent();

    @OutputFile
    public abstract RegularFileProperty getOutputFile();

    @Internal
    public Provider<String> getJson() {
        return getContent().map(Json::stringify);
    }

    @Inject
    public WriteJson(ProviderFactory providers) {
        this.providers = providers;
    }

    public void content(Action<? super JsonObjectSpec> configure) {
        configure.execute(contentSpec);
    }

    public void content(Closure<?> configure) {
        Closures.apply(configure, contentSpec);
    }

    public void from(RegularFile file, Closure<?> configure) {
        importContents(file, MapImportSpec.buildPredicate(configure));
    }

    public void from(RegularFile file, Action<? super MapImportSpec> configure) {
        importContents(file, MapImportSpec.buildPredicate(configure));
    }

    public void from(RegularFile file) {
        importContents(file, x -> true);
    }

    public void from(Provider<RegularFile> file, Closure<?> configure) {
        importContents(file, MapImportSpec.buildPredicate(configure));
    }

    public void from(Provider<RegularFile> file, Action<? super MapImportSpec> configure) {
        importContents(file, MapImportSpec.buildPredicate(configure));
    }

    public void from(Provider<RegularFile> file) {
        importContents(file, x -> true);
    }

    public void putAll(Provider<Map<String, Object>> mapProvider, Action<? super MapImportSpec> configure) {
        importMap(mapProvider, MapImportSpec.buildPredicate(configure));
    }

    public void putAll(Provider<Map<String, Object>> mapProvider, Closure<?> configure) {
        importMap(mapProvider, MapImportSpec.buildPredicate(configure));
    }

    public void putAll(Provider<Map<String, Object>> mapProvider) {
        importMap(mapProvider, x -> true);
    }

    private void importContents(Provider<RegularFile> file, Predicate<String> keyPredicate) {
        var jsonProvider = providers.fileContents(file).getAsText()
                .map(Json.mapParser(Object.class)::apply);

        importMap(jsonProvider, keyPredicate);
    }

    private void importContents(RegularFile file, Predicate<String> keyPredicate) {
        var jsonProvider = providers.fileContents(file).getAsText()
                .map(Json.mapParser(Object.class)::apply);

        importMap(jsonProvider, keyPredicate);
    }

    private void importMap(Provider<Map<String, Object>> mapProvider, Predicate<? super String> keyPredicate) {
        var filteredProvider = mapProvider.map(v -> v.entrySet().stream()
                .filter(pair -> keyPredicate.test(pair.getKey()) && Objects.nonNull(pair.getValue()))
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue,
                        (a, b) -> b,
                        LinkedHashMap::new)));

        getContent().putAll(filteredProvider);
    }

    @TaskAction
    public void run() {
        var data = getContent().getOrElse(Map.of());
        var file = getOutputFile().getAsFile().get();
        Json.write(file, data);
    }

}
