/*
 * Copyright 2014 the original author or authors.
 *
 * Licensed 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.gradle.jvm.tasks;

import groovy.lang.Closure;
import org.gradle.api.Action;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.CopySpec;
import org.gradle.api.file.FileCopyDetails;
import org.gradle.api.internal.file.collections.FileTreeAdapter;
import org.gradle.api.internal.file.collections.GeneratedSingletonFileTree;
import org.gradle.api.internal.file.copy.CopySpecInternal;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.api.java.archives.Manifest;
import org.gradle.api.java.archives.internal.CustomManifestInternalWrapper;
import org.gradle.api.java.archives.internal.DefaultManifest;
import org.gradle.api.java.archives.internal.ManifestInternal;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.bundling.Zip;
import org.gradle.util.ConfigureUtil;

import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.concurrent.Callable;

/**
 * Assembles a JAR archive.
 */
public class Jar extends Zip {

    public static final String DEFAULT_EXTENSION = "jar";
    private String manifestContentCharset = DefaultManifest.DEFAULT_CONTENT_CHARSET;
    private Manifest manifest;
    private final CopySpecInternal metaInf;

    public Jar() {
        getArchiveExtension().set(DEFAULT_EXTENSION);
        setMetadataCharset("UTF-8");

        manifest = new DefaultManifest(getFileResolver());
        // Add these as separate specs, so they are not affected by the changes to the main spec
        metaInf = (CopySpecInternal) getRootSpec().addFirst().into("META-INF");
        metaInf.addChild().from(new Callable<FileTreeAdapter>() {
            public FileTreeAdapter call() throws Exception {
                GeneratedSingletonFileTree manifestSource = new GeneratedSingletonFileTree(getTemporaryDirFactory(), "MANIFEST.MF", new Action<OutputStream>() {
                    public void execute(OutputStream outputStream) {
                        Manifest manifest = getManifest();
                        if (manifest == null) {
                            manifest = new DefaultManifest(null);
                        }
                        ManifestInternal manifestInternal;
                        if (manifest instanceof ManifestInternal) {
                            manifestInternal = (ManifestInternal) manifest;
                        } else {
                            manifestInternal = new CustomManifestInternalWrapper(manifest);
                        }
                        manifestInternal.setContentCharset(manifestContentCharset);
                        manifestInternal.writeTo(outputStream);
                    }
                });
                return new FileTreeAdapter(manifestSource);
            }
        });
        getMainSpec().appendCachingSafeCopyAction(new Action<FileCopyDetails>() {
            public void execute(FileCopyDetails details) {
                if (details.getPath().equalsIgnoreCase("META-INF/MANIFEST.MF")) {
                    details.exclude();
                }
            }
        });
    }

    /**
     * The character set used to encode JAR metadata like file names.
     * Defaults to UTF-8.
     * You can change this property but it is not recommended as JVMs expect JAR metadata to be encoded using UTF-8
     *
     * @return the character set used to encode JAR metadata like file names
     * @since 2.14
     */
    @Override
    public String getMetadataCharset() {
        return super.getMetadataCharset();
    }

    /**
     * The character set used to encode JAR metadata like file names.
     * Defaults to UTF-8.
     * You can change this property but it is not recommended as JVMs expect JAR metadata to be encoded using UTF-8
     *
     * @param metadataCharset the character set used to encode JAR metadata like file names
     * @since 2.14
     */
    @Override
    public void setMetadataCharset(String metadataCharset) {
        super.setMetadataCharset(metadataCharset);
    }

    /**
     * The character set used to encode the manifest content.
     * Defaults to UTF-8.
     * You can change this property but it is not recommended as JVMs expect manifests content to be encoded using UTF-8.
     *
     * @return the character set used to encode the manifest content
     * @since 2.14
     */
    @Input
    public String getManifestContentCharset() {
        return manifestContentCharset;
    }

    /**
     * The character set used to encode the manifest content.
     *
     * @param manifestContentCharset the character set used to encode the manifest content
     * @see #getManifestContentCharset()
     * @since 2.14
     */
    public void setManifestContentCharset(String manifestContentCharset) {
        if (manifestContentCharset == null) {
            throw new InvalidUserDataException("manifestContentCharset must not be null");
        }
        if (!Charset.isSupported(manifestContentCharset)) {
            throw new InvalidUserDataException(String.format("Charset for manifestContentCharset '%s' is not supported by your JVM", manifestContentCharset));
        }
        this.manifestContentCharset = manifestContentCharset;
    }

    /**
     * Returns the manifest for this JAR archive.
     *
     * @return The manifest
     */
    @Internal
    public Manifest getManifest() {
        return manifest;
    }

    /**
     * Sets the manifest for this JAR archive.
     *
     * @param manifest The manifest. May be null.
     */
    public void setManifest(Manifest manifest) {
        this.manifest = manifest;
    }

    /**
     * Configures the manifest for this JAR archive.
     *
     * <p>The given closure is executed to configure the manifest. The {@link org.gradle.api.java.archives.Manifest} is passed to the closure as its delegate.</p>
     *
     * @param configureClosure The closure.
     * @return This.
     */
    public Jar manifest(Closure<?> configureClosure) {
        ConfigureUtil.configure(configureClosure, forceManifest());
        return this;
    }

    /**
     * Configures the manifest for this JAR archive.
     *
     * <p>The given action is executed to configure the manifest.</p>
     *
     * @param configureAction The action.
     * @return This.
     * @since 3.5
     */
    public Jar manifest(Action<? super Manifest> configureAction) {
        configureAction.execute(forceManifest());
        return this;
    }

    private Manifest forceManifest() {
        if (manifest == null) {
            manifest = new DefaultManifest(((ProjectInternal) getProject()).getFileResolver());
        }
        return manifest;
    }

    @Internal
    public CopySpec getMetaInf() {
        return metaInf.addChild();
    }

    /**
     * Adds content to this JAR archive's META-INF directory.
     *
     * <p>The given closure is executed to configure a {@code CopySpec}. The {@link org.gradle.api.file.CopySpec} is passed to the closure as its delegate.</p>
     *
     * @param configureClosure The closure.
     * @return The created {@code CopySpec}
     */
    public CopySpec metaInf(Closure<?> configureClosure) {
        return ConfigureUtil.configure(configureClosure, getMetaInf());
    }

    /**
     * Adds content to this JAR archive's META-INF directory.
     *
     * <p>The given action is executed to configure a {@code CopySpec}.</p>
     *
     * @param configureAction The action.
     * @return The created {@code CopySpec}
     * @since 3.5
     */
    public CopySpec metaInf(Action<? super CopySpec> configureAction) {
        CopySpec metaInf = getMetaInf();
        configureAction.execute(metaInf);
        return metaInf;
    }
}
