package io.freefair.gradle.plugins.jsass;

import com.google.gson.Gson;
import io.bit3.jsass.Compiler;
import io.bit3.jsass.*;
import io.bit3.jsass.annotation.DebugFunction;
import io.bit3.jsass.annotation.ErrorFunction;
import io.bit3.jsass.annotation.WarnFunction;
import io.bit3.jsass.importer.Importer;
import lombok.Getter;
import lombok.Setter;
import okio.BufferedSink;
import okio.Okio;
import org.gradle.api.GradleException;
import org.gradle.api.UncheckedIOException;
import org.gradle.api.file.*;
import org.gradle.api.internal.plugins.DslObject;
import org.gradle.api.plugins.ExtraPropertiesExtension;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.*;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

/**
 * @author Lars Grefer
 */
@Getter
@Setter
@CacheableTask
public class SassCompile extends SourceTask {

    public SassCompile() {
        include("**/*.scss");
        include("**/*.sass");

        ExtraPropertiesExtension extraProperties = new DslObject(this).getExtensions().getExtraProperties();
        for (OutputStyle value : OutputStyle.values()) {
            extraProperties.set(value.name(), value);
        }
    }

    @OutputFiles
    protected FileTree getOutputFiles() {
        ConfigurableFileTree files = getProject().fileTree(destinationDir);
        files.include("**/*.css");
        files.include("**/*.css.map");
        return files;
    }

    @Internal
    private final DirectoryProperty destinationDir = getProject().getObjects().directoryProperty();

    @TaskAction
    public void compileSass() {
        Compiler compiler = new Compiler();
        Options options = new Options();

        options.setFunctionProviders(new ArrayList<>(getFunctionProviders()));
        options.getFunctionProviders().add(new LoggingFunctionProvider());
        options.setHeaderImporters(getHeaderImporters());
        options.setImporters(getImporters());
        if (getIncludePaths() != null) {
            options.setIncludePaths(new ArrayList<>(getIncludePaths().getFiles()));
        }
        options.setIndent(indent.get());
        options.setLinefeed(linefeed.get());
        options.setOmitSourceMapUrl(omitSourceMapUrl.get());
        options.setOutputStyle(outputStyle.get());
        options.setPluginPath(pluginPath.getOrNull());
        options.setPrecision(precision.get());
        options.setSourceComments(sourceComments.get());
        options.setSourceMapContents(sourceMapContents.get());
        options.setSourceMapEmbed(sourceMapEmbed.get());
        options.setSourceMapRoot(sourceMapRoot.getOrNull());

        getSource().visit(new FileVisitor() {
            @Override
            public void visitDir(FileVisitDetails fileVisitDetails) {

            }

            @Override
            public void visitFile(FileVisitDetails fileVisitDetails) {
                String name = fileVisitDetails.getName();
                if (name.startsWith("_"))
                    return;

                if (name.endsWith(".scss") || name.endsWith(".sass")) {
                    File in = fileVisitDetails.getFile();

                    String pathString = fileVisitDetails.getRelativePath().getPathString();

                    pathString = pathString.substring(0, pathString.length() - 5) + ".css";

                    File realOut = new File(getDestinationDir().get().getAsFile(), pathString);
                    File fakeOut = new File(
                            fileVisitDetails.getFile().getParentFile(),
                            name.substring(0, name.length() - 5) + ".css"
                    );
                    File realMap = new File(getDestinationDir().get().getAsFile(), pathString + ".map");
                    File fakeMap = new File(fakeOut.getPath() + ".map");

                    options.setIsIndentedSyntaxSrc(name.endsWith(".sass"));

                    if (sourceMapEnabled.get()) {
                        options.setSourceMapFile(fakeMap.toURI());
                    } else {
                        options.setSourceMapFile(null);
                    }

                    try {
                        URI inputPath = in.getAbsoluteFile().toURI();

                        Output output = compiler.compileFile(inputPath, fakeOut.toURI(), options);

                        if (realOut.getParentFile().exists() || realOut.getParentFile().mkdirs()) {
                            try (BufferedSink sink = Okio.buffer(Okio.sink(realOut))) {
                                sink.writeUtf8(output.getCss());
                            }
                        }
                        else {
                            getLogger().error("Cannot write into {}", realOut.getParentFile());
                            throw new GradleException("Cannot write into " + realMap.getParentFile());
                        }
                        if (sourceMapEnabled.get()) {
                            if (realMap.getParentFile().exists() || realMap.getParentFile().mkdirs()) {
                                try (BufferedSink sink = Okio.buffer(Okio.sink(realMap))) {
                                    sink.writeUtf8(output.getSourceMap());
                                }
                            }
                            else {
                                getLogger().error("Cannot write into {}", realMap.getParentFile());
                                throw new GradleException("Cannot write into " + realMap.getParentFile());
                            }
                        }
                    } catch (CompilationException e) {
                        SassError sassError = new Gson().fromJson(e.getErrorJson(), SassError.class);

                        getLogger().error("{}:{}:{}", sassError.getFile(), sassError.getLine(), sassError.getColumn());
                        getLogger().error(e.getErrorMessage());

                        throw new RuntimeException(e);
                    } catch (IOException e) {
                        getLogger().error(e.getLocalizedMessage());
                        throw new UncheckedIOException(e);
                    }
                }
            }
        });
    }

    /**
     * Custom import functions.
     */
    @Input
    @Optional
    private List<Object> functionProviders = new LinkedList<>();

    @Input
    @Optional
    private List<Importer> headerImporters = new LinkedList<>();

    /**
     * Custom import functions.
     */
    @Input
    @Optional
    private Collection<Importer> importers = new LinkedList<>();

    /**
     * SassList of paths.
     */
    @InputFiles
    @Optional
    @PathSensitive(PathSensitivity.RELATIVE)
    private final ConfigurableFileCollection includePaths = getProject().files();

    @Input
    private final Property<String> indent = getProject().getObjects().property(String.class);

    @Input
    private final Property<String> linefeed = getProject().getObjects().property(String.class);

    /**
     * Disable sourceMappingUrl in css output.
     */
    @Input
    private final Property<Boolean> omitSourceMapUrl = getProject().getObjects().property(Boolean.class);

    /**
     * Output style for the generated css code.
     */
    @Input
    private final Property<OutputStyle> outputStyle = getProject().getObjects().property(OutputStyle.class);

    @Input
    @Optional
    private final Property<String> pluginPath = getProject().getObjects().property(String.class);

    /**
     * Precision for outputting fractional numbers.
     */
    @Input
    private final Property<Integer> precision = getProject().getObjects().property(Integer.class);

    /**
     * If you want inline source comments.
     */
    @Input
    private final Property<Boolean> sourceComments = getProject().getObjects().property(Boolean.class);

    /**
     * Embed include contents in maps.
     */
    @Input
    private final Property<Boolean> sourceMapContents = getProject().getObjects().property(Boolean.class);

    /**
     * Embed sourceMappingUrl as data uri.
     */
    @Input
    private final Property<Boolean> sourceMapEmbed = getProject().getObjects().property(Boolean.class);

    @Input
    private final Property<Boolean> sourceMapEnabled = getProject().getObjects().property(Boolean.class);

    @Input
    @Optional
    private final Property<URI> sourceMapRoot = getProject().getObjects().property(URI.class);

    public void setOutputStyle(String outputStyle) {
        this.outputStyle.set(OutputStyle.valueOf(outputStyle.trim().toUpperCase()));
    }

    public class LoggingFunctionProvider {

        @WarnFunction
        @SuppressWarnings("unused")
        public void warn(String message) {
            getLogger().warn(message);
        }

        @ErrorFunction
        @SuppressWarnings("unused")
        public void error(String message) {
            getLogger().error(message);
        }

        @DebugFunction
        @SuppressWarnings("unused")
        public void debug(String message) {
            getLogger().info(message);
        }
    }
}