/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * 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 com.android.sdklib.build;

import static com.android.SdkConstants.EXT_BC;
import static com.android.SdkConstants.FN_RENDERSCRIPT_V8_JAR;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.BuildToolInfo;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * Compiles Renderscript files.
 */
public class RenderScriptProcessor {

    private static final String LIBCLCORE_BC = "libclcore.bc";

    // ABI list, as pairs of (android-ABI, toolchain-ABI)
    private static final class Abi {

        @NonNull
        private final String mDevice;
        @NonNull
        private final String mToolchain;
        @NonNull
        private final BuildToolInfo.PathId mLinker;
        @NonNull
        private final String[] mLinkerArgs;

        Abi(@NonNull String device,
            @NonNull String toolchain,
            @NonNull BuildToolInfo.PathId linker,
            @NonNull String... linkerArgs) {

            mDevice = device;
            mToolchain = toolchain;
            mLinker = linker;
            mLinkerArgs = linkerArgs;
        }
    }

    private static final Abi[] ABIS_32 = {
            new Abi("armeabi-v7a", "armv7-none-linux-gnueabi", BuildToolInfo.PathId.LD_ARM,
                    "-dynamic-linker", "/system/bin/linker", "-X", "-m", "armelf_linux_eabi"),
            new Abi("mips", "mipsel-unknown-linux", BuildToolInfo.PathId.LD_MIPS, "-EL"),
            new Abi("x86", "i686-unknown-linux", BuildToolInfo.PathId.LD_X86, "-m", "elf_i386") };

    private static final Abi[] ABIS_64 = {
            new Abi("arm64-v8a", "aarch64-linux-android", BuildToolInfo.PathId.LD_ARM64, "-X"),
            new Abi("x86_64", "x86_64-unknown-linux", BuildToolInfo.PathId.LD_X86_64, "-m", "elf_x86_64") };

    public static final String RS_DEPS = "rsDeps";

    @NonNull
    private final List<File> mInputs;

    @NonNull
    private final List<File> mImportFolders;

    @NonNull
    private final File mBuildFolder;

    @NonNull
    private final File mSourceOutputDir;

    @NonNull
    private final File mResOutputDir;

    @NonNull
    private final File mObjOutputDir;

    @NonNull
    private final File mLibOutputDir;

    @NonNull
    private final BuildToolInfo mBuildToolInfo;

    private final int mTargetApi;

    private final boolean mDebugBuild;

    private final int mOptimLevel;

    private final boolean mSupportMode;

    private final File mRsLib;
    private final Map<String, File> mLibClCore = Maps.newHashMap();

    public interface CommandLineLauncher {
        void launch(
                @NonNull File executable,
                @NonNull List<String> arguments,
                @NonNull Map<String, String> envVariableMap)
                throws IOException, InterruptedException;
    }

    public RenderScriptProcessor(
            @NonNull List<File> inputs,
            @NonNull List<File> importFolders,
            @NonNull File buildFolder,
            @NonNull File sourceOutputDir,
            @NonNull File resOutputDir,
            @NonNull File objOutputDir,
            @NonNull File libOutputDir,
            @NonNull BuildToolInfo buildToolInfo,
            int targetApi,
            boolean debugBuild,
            int optimLevel,
            boolean supportMode) {
        mInputs = inputs;
        mImportFolders = importFolders;
        mBuildFolder = buildFolder;
        mSourceOutputDir = sourceOutputDir;
        mResOutputDir = resOutputDir;
        mObjOutputDir = objOutputDir;
        mLibOutputDir = libOutputDir;
        mBuildToolInfo = buildToolInfo;
        mTargetApi = targetApi;
        mDebugBuild = debugBuild;
        mOptimLevel = optimLevel;
        mSupportMode = supportMode;

        if (supportMode) {
            File rs = new File(mBuildToolInfo.getLocation(), "renderscript");
            mRsLib = new File(rs, "lib");
            File bcFolder = new File(mRsLib, "bc");
            for (Abi abi : ABIS_32) {
                File rsClCoreFile = new File(bcFolder, abi.mDevice + File.separatorChar + LIBCLCORE_BC);
                if (rsClCoreFile.exists()) {
                    mLibClCore.put(abi.mDevice, rsClCoreFile);
                }
            }
            for (Abi abi : ABIS_64) {
                File rsClCoreFile = new File(bcFolder, abi.mDevice + File.separatorChar + LIBCLCORE_BC);
                if (rsClCoreFile.exists()) {
                    mLibClCore.put(abi.mDevice, rsClCoreFile);
                }
            }
        } else {
            mRsLib = null;
        }
    }

    public void cleanOldOutput(@Nullable Collection<File> oldOutputs) {
        if (oldOutputs != null) {
            // the old output collections contains the bc and .java files that could be
            // in a folder shared with other output files, so it's useful to delete
            // those only.

            for (File file : oldOutputs) {
                file.delete();
            }
        }

        // however .o and .so from support mode are in their own folder so we delete the
        // content of those folders directly.
        deleteFolder(mObjOutputDir);
        deleteFolder(mLibOutputDir);
    }

    public static File getSupportJar(String buildToolsFolder) {
        return new File(buildToolsFolder, "renderscript/lib/" + FN_RENDERSCRIPT_V8_JAR);
    }

    public static File getSupportNativeLibFolder(String buildToolsFolder) {
        File rs = new File(buildToolsFolder, "renderscript");
        File lib = new File(rs, "lib");
        return new File(lib, "packaged");
    }

    public void build(@NonNull CommandLineLauncher launcher)
            throws IOException, InterruptedException {

        // get the env var
        Map<String, String> env = Maps.newHashMap();
        if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
            env.put("DYLD_LIBRARY_PATH", mBuildToolInfo.getLocation().getAbsolutePath());
        } else if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX) {
            env.put("LD_LIBRARY_PATH", mBuildToolInfo.getLocation().getAbsolutePath());
        }

        doMainCompilation(launcher, env);

        if (mSupportMode) {
            createSupportFiles(launcher, env);
        }
    }

    private void doMainCompilation(@NonNull CommandLineLauncher launcher,
            @NonNull Map<String, String> env)
            throws IOException, InterruptedException {
        if (mInputs.isEmpty()) {
            return;
        }

        String renderscript = mBuildToolInfo.getPath(BuildToolInfo.PathId.LLVM_RS_CC);
        if (renderscript == null || !new File(renderscript).isFile()) {
            throw new IllegalStateException(BuildToolInfo.PathId.LLVM_RS_CC + " is missing");
        }

        String rsPath = mBuildToolInfo.getPath(BuildToolInfo.PathId.ANDROID_RS);
        String rsClangPath = mBuildToolInfo.getPath(BuildToolInfo.PathId.ANDROID_RS_CLANG);

        // the renderscript compiler doesn't expect the top res folder,
        // but the raw folder directly.
        File rawFolder = new File(mResOutputDir, SdkConstants.FD_RES_RAW);

        // compile all the files in a single pass
        ArrayList<String> command = Lists.newArrayListWithExpectedSize(25);

        // Due to a device side bug, let's not enable this at this time.
//        if (mDebugBuild) {
//            command.add("-g");
//        }

        command.add("-O");
        command.add(Integer.toString(mOptimLevel));

        // add all import paths
        command.add("-I");
        command.add(rsPath);
        command.add("-I");
        command.add(rsClangPath);

        for (File importPath : mImportFolders) {
            if (importPath.isDirectory()) {
                command.add("-I");
                command.add(importPath.getAbsolutePath());
            }
        }

        command.add("-d");
        command.add(new File(mBuildFolder, RS_DEPS).getAbsolutePath());
        command.add("-MD");

        if (mSupportMode) {
            command.add("-rs-package-name=android.support.v8.renderscript");
        }

        // source output
        command.add("-p");
        command.add(mSourceOutputDir.getAbsolutePath());

        // res output
        command.add("-o");
        command.add(rawFolder.getAbsolutePath());

        command.add("-target-api");
        int targetApi = mTargetApi < 11 ? 11 : mTargetApi;
        targetApi = (mSupportMode && targetApi < 18) ? 18 : targetApi;
        command.add(Integer.toString(targetApi));

        // input files
        for (File sourceFile : mInputs) {
            command.add(sourceFile.getAbsolutePath());
        }

        launcher.launch(new File(renderscript), command, env);
    }

    private void createSupportFiles(@NonNull CommandLineLauncher launcher,
            @NonNull Map<String, String> env) throws IOException, InterruptedException {
        // get the generated BC files.
        int targetApi = mTargetApi < 11 ? 11 : mTargetApi;
        targetApi = (mSupportMode && targetApi < 18) ? 18 : targetApi;
        if (targetApi > 21) {
            File rawFolder =  new File(mResOutputDir, SdkConstants.FD_RES_RAW);
            createSupportFilesHelper(rawFolder, ABIS_32, launcher, env);
        } else {
            File rawFolder32 =  new File(mResOutputDir, SdkConstants.FD_RES_RAW + "/bc32");
            createSupportFilesHelper(rawFolder32, ABIS_32, launcher, env);
            File rawFolder64 =  new File(mResOutputDir, SdkConstants.FD_RES_RAW + "/bc64");
            createSupportFilesHelper(rawFolder64, ABIS_64, launcher, env);
        }
    }

    private void createSupportFilesHelper(@NonNull File rawFolder,
                                          @NonNull Abi[] abis,
                                          @NonNull CommandLineLauncher launcher,
                                          @NonNull Map<String, String> env
                                          ) throws IOException, InterruptedException{
        SourceSearcher searcher = new SourceSearcher(Collections.singletonList(rawFolder), EXT_BC);
        FileGatherer fileGatherer = new FileGatherer();
        searcher.search(fileGatherer);

        for (File bcFile : fileGatherer.getFiles()) {
            String name = bcFile.getName();
            String objName = name.replaceAll("\\.bc", ".o");
            String soName = "librs." + name.replaceAll("\\.bc", ".so");

            for (Abi abi : abis) {
                // only build for the ABIs bundled in Build-Tools.
                if (mLibClCore.get(abi.mDevice) == null) {
                    continue;
                }
                File objFile = createSupportObjFile(bcFile, abi, objName, launcher, env);
                createSupportLibFile(objFile, abi, soName, launcher, env);
            }
        }
    }

    private File createSupportObjFile(
            @NonNull File bcFile,
            @NonNull Abi abi,
            @NonNull String objName,
            @NonNull CommandLineLauncher launcher,
            @NonNull Map<String, String> env) throws IOException, InterruptedException {


        // make sure the dest folder exist
        File abiFolder = new File(mObjOutputDir, abi.mDevice);
        if (!abiFolder.isDirectory() && !abiFolder.mkdirs()) {
            throw new IOException("Unable to create dir " + abiFolder.getAbsolutePath());
        }

        File exe = new File(mBuildToolInfo.getPath(BuildToolInfo.PathId.BCC_COMPAT));

        List<String> args = Lists.newArrayListWithExpectedSize(10);

        args.add("-O" + Integer.toString(mOptimLevel));

        File outFile = new File(abiFolder, objName);
        args.add("-o");
        args.add(outFile.getAbsolutePath());

        args.add("-fPIC");
        args.add("-shared");

        args.add("-rt-path");
        args.add(mLibClCore.get(abi.mDevice).getAbsolutePath());

        args.add("-mtriple");
        args.add(abi.mToolchain);

        args.add(bcFile.getAbsolutePath());

        launcher.launch(exe, args, env);

        return outFile;
    }

    private void createSupportLibFile(
            @NonNull File objFile,
            @NonNull Abi abi,
            @NonNull String soName,
            @NonNull CommandLineLauncher launcher,
            @NonNull Map<String, String> env) throws IOException, InterruptedException {

        // make sure the dest folder exist
        File abiFolder = new File(mLibOutputDir, abi.mDevice);
        if (!abiFolder.isDirectory() && !abiFolder.mkdirs()) {
            throw new IOException("Unable to create dir " + abiFolder.getAbsolutePath());
        }

        File intermediatesFolder = new File(mRsLib, "intermediates");
        File intermediatesAbiFolder = new File(intermediatesFolder, abi.mDevice);
        File packagedFolder = new File(mRsLib, "packaged");
        File packagedAbiFolder = new File(packagedFolder, abi.mDevice);

        List<String> args = Lists.newArrayListWithExpectedSize(25);

        args.add("--eh-frame-hdr");
        Collections.addAll(args, abi.mLinkerArgs);
        args.add("-shared");
        args.add("-Bsymbolic");
        args.add("-z");
        args.add("noexecstack");
        args.add("-z");
        args.add("relro");
        args.add("-z");
        args.add("now");

        File outFile = new File(abiFolder, soName);
        args.add("-o");
        args.add(outFile.getAbsolutePath());

        args.add("-L" + intermediatesAbiFolder.getAbsolutePath());
        args.add("-L" + packagedAbiFolder.getAbsolutePath());

        args.add("-soname");
        args.add(soName);

        args.add(objFile.getAbsolutePath());
        args.add(new File(intermediatesAbiFolder, "libcompiler_rt.a").getAbsolutePath());

        args.add("-lRSSupport");
        args.add("-lm");
        args.add("-lc");

        File exe = new File(mBuildToolInfo.getPath(abi.mLinker));

        launcher.launch(exe, args, env);
    }

    protected static void deleteFolder(File folder) {
        File[] files = folder.listFiles();
        if (files != null && files.length > 0) {
            for (File file : files) {
                if (file.isDirectory()) {
                    deleteFolder(file);
                } else {
                    file.delete();
                }
            }
        }

        folder.delete();
    }
}
