/*
 * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.jpackage.internal;

import static jdk.jpackage.internal.ApplicationImageUtils.createWriteAppImageFileAction;
import static jdk.jpackage.internal.util.PListWriter.writeArray;
import static jdk.jpackage.internal.util.PListWriter.writeBoolean;
import static jdk.jpackage.internal.util.PListWriter.writeBooleanOptional;
import static jdk.jpackage.internal.util.PListWriter.writeDict;
import static jdk.jpackage.internal.util.PListWriter.writeKey;
import static jdk.jpackage.internal.util.PListWriter.writeString;
import static jdk.jpackage.internal.util.PListWriter.writeStringArray;
import static jdk.jpackage.internal.util.PListWriter.writeStringOptional;
import static jdk.jpackage.internal.util.XmlUtils.toXmlConsumer;
import static jdk.jpackage.internal.util.function.ThrowingBiConsumer.toBiConsumer;
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;

import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import jdk.jpackage.internal.PackagingPipeline.AppImageBuildEnv;
import jdk.jpackage.internal.PackagingPipeline.ApplicationImageTaskAction;
import jdk.jpackage.internal.PackagingPipeline.BuildApplicationTaskID;
import jdk.jpackage.internal.PackagingPipeline.CopyAppImageTaskID;
import jdk.jpackage.internal.PackagingPipeline.PackageBuildEnv;
import jdk.jpackage.internal.PackagingPipeline.PackageTaskID;
import jdk.jpackage.internal.PackagingPipeline.PrimaryTaskID;
import jdk.jpackage.internal.PackagingPipeline.TaskAction;
import jdk.jpackage.internal.PackagingPipeline.TaskContext;
import jdk.jpackage.internal.PackagingPipeline.TaskID;
import jdk.jpackage.internal.model.AppImageLayout;
import jdk.jpackage.internal.model.AppImageSigningConfig;
import jdk.jpackage.internal.model.Application;
import jdk.jpackage.internal.model.ApplicationLayout;
import jdk.jpackage.internal.model.FileAssociation;
import jdk.jpackage.internal.model.MacApplication;
import jdk.jpackage.internal.model.MacFileAssociation;
import jdk.jpackage.internal.model.MacPackage;
import jdk.jpackage.internal.model.Package;
import jdk.jpackage.internal.model.PackageType;
import jdk.jpackage.internal.model.PackagerException;
import jdk.jpackage.internal.util.PathUtils;
import jdk.jpackage.internal.util.function.ThrowingConsumer;

final class MacPackagingPipeline {

    enum MacBuildApplicationTaskID implements TaskID {
        RUNTIME_INFO_PLIST,
        COPY_JLILIB,
        APP_ICON,
        PKG_INFO_FILE,
        FA_ICONS,
        APP_INFO_PLIST,
        PACKAGE_FILE,
        SIGN
    }

    enum MacCopyAppImageTaskID implements TaskID {
        COPY_PACKAGE_FILE,
        COPY_RUNTIME_INFO_PLIST,
        REPLACE_APP_IMAGE_FILE,
        COPY_SIGN
    }

    static AppImageLayout packagingLayout(Package pkg) {
        return pkg.appImageLayout().resolveAt(pkg.relativeInstallDir().getFileName());
    }

    static PackagingPipeline.Builder build(Optional<Package> pkg) {
        final var builder = PackagingPipeline.buildStandard()
                .appContextMapper(appContext -> {
                    return new TaskContextProxy(appContext, true, false);
                })
                .pkgContextMapper(appContext -> {
                    final var isRuntimeInstaller = pkg.map(Package::isRuntimeInstaller).orElse(false);
                    final var withPredefinedAppImage = pkg.flatMap(Package::predefinedAppImage).isPresent();
                    return new TaskContextProxy(appContext, false, isRuntimeInstaller || withPredefinedAppImage);
                })
                .appImageLayoutForPackaging(MacPackagingPipeline::packagingLayout)
                .task(PackageTaskID.RUN_POST_IMAGE_USER_SCRIPT)
                        .packageAction(MacPackagingPipeline::runPostAppImageUserScript).add()
                .task(CopyAppImageTaskID.COPY)
                        .copyAction(MacPackagingPipeline::copyAppImage).add()
                .task(MacBuildApplicationTaskID.RUNTIME_INFO_PLIST)
                        .applicationAction(MacPackagingPipeline::writeApplicationRuntimeInfoPlist)
                        .addDependent(BuildApplicationTaskID.CONTENT).add()
                .task(MacBuildApplicationTaskID.COPY_JLILIB)
                        .applicationAction(MacPackagingPipeline::copyJliLib)
                        .addDependency(BuildApplicationTaskID.RUNTIME)
                        .addDependent(BuildApplicationTaskID.CONTENT).add()
                .task(MacBuildApplicationTaskID.APP_ICON)
                        .applicationAction(new ApplicationIcon())
                        .addDependent(BuildApplicationTaskID.CONTENT).add()
                .task(MacBuildApplicationTaskID.PKG_INFO_FILE)
                        .applicationAction(MacPackagingPipeline::writePkgInfoFile)
                        .addDependent(BuildApplicationTaskID.CONTENT).add()
                .task(MacBuildApplicationTaskID.PACKAGE_FILE)
                        .packageAction(MacPackagingPipeline::writePackageFile)
                        .addDependents(BuildApplicationTaskID.CONTENT).add()
                .task(MacCopyAppImageTaskID.REPLACE_APP_IMAGE_FILE)
                        .addDependent(PrimaryTaskID.COPY_APP_IMAGE)
                        .noaction().add()
                .task(MacCopyAppImageTaskID.COPY_PACKAGE_FILE)
                        .packageAction(MacPackagingPipeline::writePackageFile)
                        .addDependencies(CopyAppImageTaskID.COPY)
                        .addDependents(PrimaryTaskID.COPY_APP_IMAGE).add()
                .task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST)
                        .addDependencies(CopyAppImageTaskID.COPY)
                        .addDependents(PrimaryTaskID.COPY_APP_IMAGE).add()
                .task(MacBuildApplicationTaskID.FA_ICONS)
                        .applicationAction(MacPackagingPipeline::writeFileAssociationIcons)
                        .addDependent(BuildApplicationTaskID.CONTENT).add()
                .task(MacBuildApplicationTaskID.APP_INFO_PLIST)
                        .applicationAction(MacPackagingPipeline::writeAppInfoPlist)
                        .addDependent(BuildApplicationTaskID.CONTENT).add();

        builder.task(MacBuildApplicationTaskID.SIGN)
                .appImageAction(MacPackagingPipeline::sign)
                .addDependencies(builder.taskGraphSnapshot().getAllTailsOf(PrimaryTaskID.BUILD_APPLICATION_IMAGE))
                .addDependent(PrimaryTaskID.BUILD_APPLICATION_IMAGE)
                .add();

        builder.task(MacCopyAppImageTaskID.COPY_SIGN)
                .appImageAction(MacPackagingPipeline::sign)
                .addDependencies(builder.taskGraphSnapshot().getAllTailsOf(PrimaryTaskID.COPY_APP_IMAGE))
                .addDependent(PrimaryTaskID.COPY_APP_IMAGE)
                .add();

        pkg.ifPresent(p -> {
            final List<TaskID> disabledTasks = new ArrayList<>();

            if (p.type() instanceof SignAppImagePackageType) {
                // This is a phony package signing predefined app image.
                // Don't create ".package" file.
                // Don't copy predefined app image, update it in place.
                // Disable running user script after app image ready.
                // Replace ".jpackage.xml" file.
                // Use app image layout.
                disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE);
                disabledTasks.add(CopyAppImageTaskID.COPY);
                disabledTasks.add(PackageTaskID.RUN_POST_IMAGE_USER_SCRIPT);
                builder.task(MacCopyAppImageTaskID.REPLACE_APP_IMAGE_FILE).applicationAction(createWriteAppImageFileAction()).add();
                builder.appImageLayoutForPackaging(Package::appImageLayout);
            } else if (p.isRuntimeInstaller() || ((MacPackage)p).predefinedAppImageSigned().orElse(false)) {
                // If this is a runtime package or a signed predefined app image,
                // don't create ".package" file and don't sign it.
                disabledTasks.add(MacCopyAppImageTaskID.COPY_PACKAGE_FILE);
                disabledTasks.add(MacCopyAppImageTaskID.COPY_SIGN);
//                if (p.isRuntimeInstaller()) {
//                    builder.task(MacCopyAppImageTaskID.COPY_RUNTIME_INFO_PLIST).packageAction(MacPackagingPipeline::writeRuntimeRuntimeInfoPlist).add();
//                }
            }

            for (final var taskId : disabledTasks) {
                builder.task(taskId).noaction().add();
            }
        });

        return builder;
    }

    enum SignAppImagePackageType implements PackageType {
        VALUE;
    }

    static Package createSignAppImagePackage(MacApplication app, BuildEnv env) {
        if (!app.sign()) {
            throw new IllegalArgumentException();
        }
        return toSupplier(() -> {
            return new PackageBuilder(app, SignAppImagePackageType.VALUE).predefinedAppImage(
                    Objects.requireNonNull(env.appImageDir())).installDir(Path.of("/foo")).create();
        }).get();
    }

    private static void copyAppImage(MacPackage pkg, AppImageDesc srcAppImage,
            AppImageDesc dstAppImage) throws IOException {
        PackagingPipeline.copyAppImage(srcAppImage, dstAppImage, !pkg.predefinedAppImageSigned().orElse(false));
    }

    private static void copyJliLib(
            AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {

        final var runtimeMacOSDir = env.resolvedLayout().runtimeRootDirectory().resolve("Contents/MacOS");

        final var jliName = Path.of("libjli.dylib");

        try (var walk = Files.walk(env.resolvedLayout().runtimeDirectory().resolve("lib"))) {
            final var jli = walk
                    .filter(file -> file.getFileName().equals(jliName))
                    .findFirst()
                    .orElseThrow();
            Files.createDirectories(runtimeMacOSDir);
            Files.copy(jli, runtimeMacOSDir.resolve(jliName));
        }
    }

    private static void runPostAppImageUserScript(PackageBuildEnv<Package, AppImageLayout> env) throws IOException {
        PackagingPipeline.runPostAppImageUserScript(new PackageBuildEnv<>(
                BuildEnv.withAppImageDir(env.env(), env.env().appImageDir().resolve(env.envLayout().rootDirectory())),
                env.pkg(), env.pkg().appImageLayout(), env.outputDir()));
    }

    private static void writePackageFile(PackageBuildEnv<Package, ApplicationLayout> env) throws IOException {
        new PackageFile(env.pkg().packageName()).save(env.resolvedLayout());
    }

    private static void writePkgInfoFile(
            AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {

        final var dir = env.resolvedLayout().contentDirectory();
        Files.createDirectories(dir);
        Files.write(dir.resolve("PkgInfo"),
                "APPL????".getBytes(StandardCharsets.ISO_8859_1));
    }

    private static void writeRuntimeRuntimeInfoPlist(PackageBuildEnv<MacPackage, AppImageLayout> env) throws IOException {
        writeRuntimeInfoPlist(env.pkg().app(), env.env(), env.resolvedLayout().rootDirectory());
    }

    private static void writeApplicationRuntimeInfoPlist(
            AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {
        writeRuntimeInfoPlist(env.app(), env.env(), env.resolvedLayout().runtimeRootDirectory());
    }

    private static void writeRuntimeInfoPlist(MacApplication app, BuildEnv env, Path runtimeRootDirectory) throws IOException {

        Map<String, String> data = new HashMap<>();
        data.put("CF_BUNDLE_IDENTIFIER", app.bundleIdentifier());
        data.put("CF_BUNDLE_NAME", app.bundleName());
        data.put("CF_BUNDLE_VERSION", app.version());
        data.put("CF_BUNDLE_SHORT_VERSION_STRING", app.shortVersion().toString());

        env.createResource("Runtime-Info.plist.template")
                .setPublicName("Runtime-Info.plist")
                .setCategory(I18N.getString("resource.runtime-info-plist"))
                .setSubstitutionData(data)
                .saveToFile(runtimeRootDirectory.resolve("Contents/Info.plist"));
    }

    private static void writeAppInfoPlist(
            AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {

        final var app = env.app();

        final var infoPlistFile = env.resolvedLayout().contentDirectory().resolve("Info.plist");

        Log.verbose(I18N.format("message.preparing-info-plist", PathUtils.normalizedAbsolutePathString(infoPlistFile)));

        final String faXml = toSupplier(() -> {
            var buf = new StringWriter();
            var xml = XMLOutputFactory.newInstance().createXMLStreamWriter(buf);
            writeCFBundleDocumentTypes(xml, app);
            writeUTExportedTypeDeclarations(xml, app);
            xml.flush();
            xml.close();
            return buf.toString();
        }).get();

        Map<String, String> data = new HashMap<>();
        data.put("DEPLOY_ICON_FILE", ApplicationIcon.getPath(app, env.resolvedLayout()).getFileName().toString());
        data.put("DEPLOY_BUNDLE_COPYRIGHT", app.copyright());
        data.put("DEPLOY_LAUNCHER_NAME", app.mainLauncher().orElseThrow().executableNameWithSuffix());
        data.put("DEPLOY_BUNDLE_SHORT_VERSION", app.shortVersion().toString());
        data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", app.version());
        data.put("DEPLOY_BUNDLE_NAME", app.bundleName());
        data.put("DEPLOY_BUNDLE_IDENTIFIER", app.bundleIdentifier());
        data.put("DEPLOY_APP_CATEGORY", app.category());
        data.put("DEPLOY_FILE_ASSOCIATIONS", faXml);

        env.env().createResource("Info-lite.plist.template")
                .setCategory(I18N.getString("resource.app-info-plist"))
                .setSubstitutionData(data)
                .setPublicName("Info.plist")
                .saveToFile(infoPlistFile);
    }

    private static void sign(AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {

        final var app = env.app();

        final var codesignConfigBuilder = CodesignConfig.build();
        app.signingConfig().ifPresent(codesignConfigBuilder::from);

        if (app.sign() && app.signingConfig().flatMap(AppImageSigningConfig::entitlements).isEmpty()) {
            final var entitlementsDefaultResource = app.signingConfig().map(
                    AppImageSigningConfig::entitlementsResourceName).orElseThrow();

            final var entitlementsFile = env.env().configDir().resolve(app.name() + ".entitlements");

            env.env().createResource(entitlementsDefaultResource)
                    .setCategory(I18N.getString("resource.entitlements"))
                    .saveToFile(entitlementsFile);

            codesignConfigBuilder.entitlements(entitlementsFile);
        }

        final Runnable signAction = () -> {
            final var appImageDir = env.resolvedLayout().rootDirectory();
            AppImageSigner.createSigner(app, codesignConfigBuilder.create()).accept(appImageDir);
        };

        app.signingConfig().flatMap(AppImageSigningConfig::keychain).map(Keychain::new).ifPresentOrElse(keychain -> {
            toBiConsumer(TempKeychain::withKeychain).accept(unused -> signAction.run(), keychain);
        }, signAction);
    }

    private static void writeCFBundleDocumentTypes(XMLStreamWriter xml,
            MacApplication app) throws XMLStreamException, IOException {
        final var fas = app.fileAssociations().toList();
        if (!fas.isEmpty()) {
            writeKey(xml, "CFBundleDocumentTypes");
            writeArray(xml, toXmlConsumer(() -> {
                for (var fa : fas) {
                    writeDict(xml, toXmlConsumer(() -> {
                        addFaToCFBundleDocumentTypes(xml, app, (MacFileAssociation) fa);
                    }));
                }
            }));
        }
    }

    private static void writeUTExportedTypeDeclarations(XMLStreamWriter xml,
            MacApplication app) throws XMLStreamException, IOException {
        final var fas = app.fileAssociations().toList();
        if (!fas.isEmpty()) {
            writeKey(xml, "UTExportedTypeDeclarations");
            writeArray(xml, toXmlConsumer(() -> {
                for (var fa : fas) {
                    writeDict(xml, toXmlConsumer(() -> {
                        addFaToUTExportedTypeDeclarations(xml, app, (MacFileAssociation) fa);
                    }));
                }
            }));
        }
    }

    private static String faContentType(MacApplication app, MacFileAssociation fa) {
        return String.format("%s.%s", app.bundleIdentifier(), fa.extension());
    }

    private static void faWriteIcon(XMLStreamWriter xml, String key, FileAssociation fa)
            throws XMLStreamException {
        fa.icon().ifPresent(ThrowingConsumer.toConsumer(icon -> {
            writeString(xml, key, icon.getFileName());
        }));
    }

    private static void addFaToCFBundleDocumentTypes(XMLStreamWriter xml,
            MacApplication app, MacFileAssociation fa) throws XMLStreamException, IOException {

        writeStringArray(xml, "LSItemContentTypes", List.of(faContentType(app, fa)));
        writeStringOptional(xml, "CFBundleTypeName", fa.description());
        writeString(xml, "LSHandlerRank", fa.lsHandlerRank());
        writeString(xml, "CFBundleTypeRole", fa.cfBundleTypeRole());
        writeStringOptional(xml, "NSPersistentStoreTypeKey", fa.nsPersistentStoreTypeKey());
        writeStringOptional(xml, "NSDocumentClass", fa.nsDocumentClass());
        writeBoolean(xml, "LSIsAppleDefaultForType", true);
        writeBooleanOptional(xml, "LSTypeIsPackage", fa.lsTypeIsPackage());
        writeBooleanOptional(xml, "LSSupportsOpeningDocumentsInPlace", fa.lsSupportsOpeningDocumentsInPlace());
        writeBooleanOptional(xml, "UISupportsDocumentBrowser", fa.uiSupportsDocumentBrowser());
        faWriteIcon(xml, "CFBundleTypeIconFile", fa);
    }

    private static void addFaToUTExportedTypeDeclarations(XMLStreamWriter xml,
            MacApplication app, MacFileAssociation fa) throws XMLStreamException, IOException {
        writeString(xml, "UTTypeIdentifier", faContentType(app, fa));
        writeStringOptional(xml, "UTTypeDescription", fa.description());
        writeStringArray(xml, "UTTypeConformsTo", fa.utTypeConformsTo());
        faWriteIcon(xml, "UTTypeIconFile", fa);

        writeKey(xml, "UTTypeTagSpecification");
        writeDict(xml, toXmlConsumer(() -> {
            writeStringArray(xml, "public.filename-extension", List.of(fa.extension()));
            writeStringArray(xml, "public.mime-type", List.of(fa.mimeType()));
            writeStringArray(xml, "NSExportableTypes", fa.nsExportableTypes());
        }));
    }

    private static class ApplicationIcon implements ApplicationImageTaskAction<MacApplication, MacApplicationLayout> {
        static Path getPath(Application app, ApplicationLayout appLayout) {
            return appLayout.desktopIntegrationDirectory().resolve(app.name() + ".icns");
        }

        @Override
        public void execute(AppImageBuildEnv<MacApplication, MacApplicationLayout> env)
                throws IOException {
            final var resource = env.env().createResource("JavaApp.icns").setCategory("icon");

            env.app().icon().ifPresent(resource::setExternal);

            resource.saveToFile(getPath(env.app(), env.resolvedLayout()));
        }
    }

    private static void writeFileAssociationIcons(AppImageBuildEnv<MacApplication, MacApplicationLayout> env) throws IOException {
        for (var faIcon : env.app().fileAssociations()
                .filter(FileAssociation::hasIcon)
                .map(FileAssociation::icon)
                .map(Optional::get).toList()) {
            Files.copy(faIcon, env.resolvedLayout().desktopIntegrationDirectory().resolve(faIcon.getFileName()));
        }
    }

    private record TaskContextProxy(TaskContext delegate, boolean forApp, boolean copyAppImage) implements TaskContext {

        @Override
        public boolean test(TaskID taskID) {
            if (!delegate.test(taskID)) {
                return false;
            } else if (taskID == MacBuildApplicationTaskID.PACKAGE_FILE) {
                // Don't create files relevant for package bundling when bundling app image
                return !forApp;
            } else {
                return true;
            }
        }

        @Override
        public void execute(TaskAction taskAction) throws IOException, PackagerException {
            delegate.execute(taskAction);
        }
    }

    static final MacApplicationLayout APPLICATION_LAYOUT = MacApplicationLayout.create(
            ApplicationLayoutUtils.PLATFORM_APPLICATION_LAYOUT, Path.of("Contents/runtime"));
}
