/*
 *  Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
 *  Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
 *
 *  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
 *
 *       https://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 brut.apktool;

import brut.androlib.*;
import brut.androlib.exceptions.AndrolibException;
import brut.androlib.exceptions.CantFindFrameworkResException;
import brut.androlib.exceptions.InFileNotFoundException;
import brut.androlib.exceptions.OutDirExistsException;
import brut.androlib.res.Framework;
import brut.common.BrutException;
import brut.directory.DirectoryException;
import brut.directory.ExtFile;
import brut.util.AaptManager;
import brut.util.OSDetection;
import org.apache.commons.cli.*;

import java.io.*;
import java.util.logging.*;

/**
 * Main entry point of the apktool.
 */
public class Main {
    public static void main(String[] args) throws BrutException {

        // headless
        System.setProperty("java.awt.headless", "true");

        // Ignore stricter validation on zip files from java 11 onwards as this is a protection technique
        // that applications use to thwart disassembly tools. We have protections in place for directory traversal
        // and handling of bogus data in the zip header, so we can ignore this.
        System.setProperty("jdk.nio.zipfs.allowDotZipEntry", "true");
        System.setProperty("jdk.util.zip.disableZip64ExtraFieldValidation", "true");

        // set verbosity default
        Verbosity verbosity = Verbosity.NORMAL;

        // cli parser
        CommandLineParser parser = new DefaultParser();
        CommandLine commandLine;

        // load options
        _Options();

        try {
            commandLine = parser.parse(allOptions, args, false);

            if (! OSDetection.is64Bit()) {
                System.err.println("32 bit support is deprecated. Apktool will not support 32bit on v3.0.0.");
            }
        } catch (ParseException ex) {
            System.err.println(ex.getMessage());
            usage();
            System.exit(1);
            return;
        }

        // check for verbose / quiet
        if (commandLine.hasOption("-v") || commandLine.hasOption("--verbose")) {
            verbosity = Verbosity.VERBOSE;
        } else if (commandLine.hasOption("-q") || commandLine.hasOption("--quiet")) {
            verbosity = Verbosity.QUIET;
        }
        setupLogging(verbosity);

        // check for advance mode
        if (commandLine.hasOption("advance") || commandLine.hasOption("advanced")) {
            setAdvanceMode();
        }

        Config config = Config.getDefaultConfig();
        initConfig(commandLine, config);

        boolean cmdFound = false;
        for (String opt : commandLine.getArgs()) {
            if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")) {
                cmdDecode(commandLine, config);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("b") || opt.equalsIgnoreCase("build")) {
                cmdBuild(commandLine, config);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("if") || opt.equalsIgnoreCase("install-framework")) {
                cmdInstallFramework(commandLine, config);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("empty-framework-dir")) {
                cmdEmptyFrameworkDirectory(commandLine, config);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("list-frameworks")) {
                cmdListFrameworks(commandLine, config);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("publicize-resources")) {
                cmdPublicizeResources(commandLine, config);
                cmdFound = true;
            }
        }

        // if no commands ran, run the version / usage check.
        if (!cmdFound) {
            if (commandLine.hasOption("version")) {
                _version();
                System.exit(0);
            } else {
                usage();
            }
        }
    }

    private static void initConfig(CommandLine cli, Config config) {
        if (cli.hasOption("p") || cli.hasOption("frame-path")) {
            config.frameworkDirectory = cli.getOptionValue("p");
        }
        if (cli.hasOption("t") || cli.hasOption("tag")) {
            config.frameworkTag = cli.getOptionValue("t");
        }
        if (cli.hasOption("api") || cli.hasOption("api-level")) {
            config.apiLevel = Integer.parseInt(cli.getOptionValue("api"));
        }
    }

    private static void cmdDecode(CommandLine cli, Config config) throws AndrolibException {
        String apkName = getLastArg(cli);

        // check decode options
        if (cli.hasOption("s") || cli.hasOption("no-src")) {
            config.setDecodeSources(Config.DECODE_SOURCES_NONE);
        }
        if (cli.hasOption("only-main-classes")) {
            config.setDecodeSources(Config.DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES);
        }
        if (cli.hasOption("d") || cli.hasOption("debug")) {
            System.err.println("SmaliDebugging has been removed in 2.1.0 onward. Please see: https://github.com/iBotPeaches/Apktool/issues/1061");
            System.exit(1);
        }
        if (cli.hasOption("b") || cli.hasOption("no-debug-info")) {
            config.baksmaliDebugMode = false;
        }
        if (cli.hasOption("f") || cli.hasOption("force")) {
            config.forceDelete = true;
        }
        if (cli.hasOption("r") || cli.hasOption("no-res")) {
            config.setDecodeResources(Config.DECODE_RESOURCES_NONE);
        }
        if (cli.hasOption("force-manifest")) {
            config.setForceDecodeManifest(Config.FORCE_DECODE_MANIFEST_FULL);
        }
        if (cli.hasOption("no-assets")) {
            config.setDecodeAssets(Config.DECODE_ASSETS_NONE);
        }
        if (cli.hasOption("k") || cli.hasOption("keep-broken-res")) {
            config.keepBrokenResources = true;
        }
        if (cli.hasOption("m") || cli.hasOption("match-original")) {
            config.analysisMode = true;
        }

        File outDir;
        if (cli.hasOption("o") || cli.hasOption("output")) {
            outDir = new File(cli.getOptionValue("o"));
        } else {
            // make out folder manually using name of apk
            String outName = apkName;
            outName = outName.endsWith(".apk") ? outName.substring(0,
                    outName.length() - 4).trim() : outName + ".out";

            // make file from path
            outName = new File(outName).getName();
            outDir = new File(outName);
        }

        ExtFile apkFile = new ExtFile(apkName);
        ApkDecoder decoder = new ApkDecoder(config, apkFile);

        try {
            decoder.decode(outDir);
        } catch (OutDirExistsException ex) {
            System.err
                    .println("Destination directory ("
                            + outDir.getAbsolutePath()
                            + ") "
                            + "already exists. Use -f switch if you want to overwrite it.");
            System.exit(1);
        } catch (InFileNotFoundException ex) {
            System.err.println("Input file (" + apkFile.getAbsolutePath() + ") " + "was not found or was not readable.");
            System.exit(1);
        } catch (CantFindFrameworkResException ex) {
            System.err
                    .println("Can't find framework resources for package of id: "
                            + ex.getPkgId()
                            + ". You must install proper "
                            + "framework files, see project website for more info.");
            System.exit(1);
        } catch (IOException ex) {
            System.err.println("Could not modify file. Please ensure you have permission.");
            System.exit(1);
        } catch (DirectoryException ex) {
            System.err.println("Could not modify internal dex files. Please ensure you have permission.");
            System.exit(1);
        }
    }

    private static void cmdBuild(CommandLine cli, Config config) {
        String[] args = cli.getArgs();
        String appDirName = args.length < 2 ? "." : args[1];

        // check for build options
        if (cli.hasOption("f") || cli.hasOption("force-all")) {
            config.forceBuildAll = true;
        }
        if (cli.hasOption("d") || cli.hasOption("debug")) {
            config.debugMode = true;
        }
        if (cli.hasOption("n") || cli.hasOption("net-sec-conf")) {
            config.netSecConf = true;
        }
        if (cli.hasOption("v") || cli.hasOption("verbose")) {
            config.verbose = true;
        }
        if (cli.hasOption("a") || cli.hasOption("aapt")) {
            config.aaptPath = cli.getOptionValue("a");
        }
        if (cli.hasOption("c") || cli.hasOption("copy-original")) {
            config.copyOriginalFiles = true;
        }
        if (cli.hasOption("nc") || cli.hasOption("no-crunch")) {
            config.noCrunch = true;
        }

        // Temporary flag to enable the use of aapt2. This will transform in time to a use-aapt1 flag, which will be
        // legacy and eventually removed.
        if (cli.hasOption("use-aapt2")) {
            config.useAapt2 = true;
        }

        File outFile;
        if (cli.hasOption("o") || cli.hasOption("output")) {
            outFile = new File(cli.getOptionValue("o"));
        } else {
            outFile = null;
        }

        if (config.netSecConf && !config.useAapt2) {
            System.err.println("-n / --net-sec-conf is only supported with --use-aapt2.");
            System.exit(1);
        }

        // try and build apk
        try {
            if (cli.hasOption("a") || cli.hasOption("aapt")) {
                config.aaptVersion = AaptManager.getAaptVersion(cli.getOptionValue("a"));
            }
            new ApkBuilder(config, new ExtFile(appDirName)).build(outFile);
        } catch (BrutException ex) {
            System.err.println(ex.getMessage());
            System.exit(1);
        }
    }

    private static void cmdInstallFramework(CommandLine cli, Config config) throws AndrolibException {
        String apkName = getLastArg(cli);
        new Framework(config).installFramework(new File(apkName));
    }

    private static void cmdListFrameworks(CommandLine cli, Config config) throws AndrolibException {
        new Framework(config).listFrameworkDirectory();
    }

    private static void cmdPublicizeResources(CommandLine cli, Config config) throws AndrolibException {
        String apkName = getLastArg(cli);
        new Framework(config).publicizeResources(new File(apkName));
    }

    private static void cmdEmptyFrameworkDirectory(CommandLine cli, Config config) throws AndrolibException {
        if (cli.hasOption("f") || cli.hasOption("force")) {
            config.forceDeleteFramework = true;
        }
        new Framework(config).emptyFrameworkDirectory();
    }

    private static String getLastArg(CommandLine cli) {
        int paraCount = cli.getArgList().size();
        return cli.getArgList().get(paraCount - 1);
    }

    private static void _version() {
        System.out.println(ApktoolProperties.getVersion());
    }

    private static void _Options() {

        // create options
        Option versionOption = Option.builder("version")
                .longOpt("version")
                .desc("Print the version.")
                .build();

        Option advanceOption = Option.builder("advance")
                .longOpt("advanced")
                .desc("Print advanced information.")
                .build();

        Option noSrcOption = Option.builder("s")
                .longOpt("no-src")
                .desc("Do not decode sources.")
                .build();

        Option onlyMainClassesOption = Option.builder()
                .longOpt("only-main-classes")
                .desc("Only disassemble the main dex classes (classes[0-9]*.dex) in the root.")
                .build();

        Option noResOption = Option.builder("r")
                .longOpt("no-res")
                .desc("Do not decode resources.")
                .build();

        Option forceManOption = Option.builder()
                .longOpt("force-manifest")
                .desc("Decode the APK's compiled manifest, even if decoding of resources is set to \"false\".")
                .build();

        Option noAssetOption = Option.builder()
                .longOpt("no-assets")
                .desc("Do not decode assets.")
                .build();

        Option debugDecOption = Option.builder("d")
                .longOpt("debug")
                .desc("REMOVED (DOES NOT WORK): Decode in debug mode.")
                .build();

        Option analysisOption = Option.builder("m")
                .longOpt("match-original")
                .desc("Keep files to closest to original as possible (prevents rebuild).")
                .build();

        Option apiLevelOption = Option.builder("api")
                .longOpt("api-level")
                .desc("The numeric api-level of the file to generate, e.g. 14 for ICS.")
                .hasArg(true)
                .argName("API")
                .build();

        Option debugBuiOption = Option.builder("d")
                .longOpt("debug")
                .desc("Set android:debuggable to \"true\" in the APK's compiled manifest.")
                .build();

        Option netSecConfOption = Option.builder("n")
                .longOpt("net-sec-conf")
                .desc("Add a generic Network Security Configuration file in the output APK")
                .build();

        Option noDbgOption = Option.builder("b")
                .longOpt("no-debug-info")
                .desc("Do not write out debug info (.local, .param, .line, etc.)")
                .build();

        Option forceDecOption = Option.builder("f")
                .longOpt("force")
                .desc("Force delete destination directory.")
                .build();

        Option frameTagOption = Option.builder("t")
                .longOpt("frame-tag")
                .desc("Use framework files tagged by <tag>.")
                .hasArg(true)
                .argName("tag")
                .build();

        Option frameDirOption = Option.builder("p")
                .longOpt("frame-path")
                .desc("Use framework files located in <dir>.")
                .hasArg(true)
                .argName("dir")
                .build();

        Option frameIfDirOption = Option.builder("p")
                .longOpt("frame-path")
                .desc("Store framework files into <dir>.")
                .hasArg(true)
                .argName("dir")
                .build();

        Option keepResOption = Option.builder("k")
                .longOpt("keep-broken-res")
                .desc("Use if there was an error and some resources were dropped, e.g.\n"
                        + "            \"Invalid config flags detected. Dropping resources\", but you\n"
                        + "            want to decode them anyway, even with errors. You will have to\n"
                        + "            fix them manually before building.")
                .build();

        Option forceBuiOption = Option.builder("f")
                .longOpt("force-all")
                .desc("Skip changes detection and build all files.")
                .build();

        Option aaptOption = Option.builder("a")
                .longOpt("aapt")
                .hasArg(true)
                .argName("loc")
                .desc("Load aapt from specified location.")
                .build();

        Option aapt2Option = Option.builder()
                .longOpt("use-aapt2")
                .desc("Use aapt2 binary instead of aapt1 during the build step.")
                .build();

        Option originalOption = Option.builder("c")
                .longOpt("copy-original")
                .desc("Copy original AndroidManifest.xml and META-INF. See project page for more info.")
                .build();

        Option noCrunchOption = Option.builder("nc")
                .longOpt("no-crunch")
                .desc("Disable crunching of resource files during the build step.")
                .build();

        Option tagOption = Option.builder("t")
                .longOpt("tag")
                .desc("Tag frameworks using <tag>.")
                .hasArg(true)
                .argName("tag")
                .build();

        Option outputBuiOption = Option.builder("o")
                .longOpt("output")
                .desc("The name of apk that gets written. (default: dist/name.apk)")
                .hasArg(true)
                .argName("dir")
                .build();

        Option outputDecOption = Option.builder("o")
                .longOpt("output")
                .desc("The name of folder that gets written. (default: apk.out)")
                .hasArg(true)
                .argName("dir")
                .build();

        Option quietOption = Option.builder("q")
                .longOpt("quiet")
                .build();

        Option verboseOption = Option.builder("v")
                .longOpt("verbose")
                .build();

        // check for advance mode
        if (isAdvanceMode()) {
            decodeOptions.addOption(noDbgOption);
            decodeOptions.addOption(keepResOption);
            decodeOptions.addOption(analysisOption);
            decodeOptions.addOption(onlyMainClassesOption);
            decodeOptions.addOption(apiLevelOption);
            decodeOptions.addOption(noAssetOption);
            decodeOptions.addOption(forceManOption);

            buildOptions.addOption(apiLevelOption);
            buildOptions.addOption(debugBuiOption);
            buildOptions.addOption(netSecConfOption);
            buildOptions.addOption(aaptOption);
            buildOptions.addOption(originalOption);
            buildOptions.addOption(aapt2Option);
            buildOptions.addOption(noCrunchOption);
        }

        // add global options
        normalOptions.addOption(versionOption);
        normalOptions.addOption(advanceOption);

        // add basic decode options
        decodeOptions.addOption(frameTagOption);
        decodeOptions.addOption(outputDecOption);
        decodeOptions.addOption(frameDirOption);
        decodeOptions.addOption(forceDecOption);
        decodeOptions.addOption(noSrcOption);
        decodeOptions.addOption(noResOption);

        // add basic build options
        buildOptions.addOption(outputBuiOption);
        buildOptions.addOption(frameDirOption);
        buildOptions.addOption(forceBuiOption);

        // add basic framework options
        frameOptions.addOption(tagOption);
        frameOptions.addOption(frameIfDirOption);

        // add empty framework options
        emptyFrameworkOptions.addOption(forceDecOption);
        emptyFrameworkOptions.addOption(frameIfDirOption);

        // add list framework options
        listFrameworkOptions.addOption(frameIfDirOption);

        // add all, loop existing cats then manually add advance
        for (Option op : normalOptions.getOptions()) {
            allOptions.addOption(op);
        }
        for (Option op : decodeOptions.getOptions()) {
            allOptions.addOption(op);
        }
        for (Option op : buildOptions.getOptions()) {
            allOptions.addOption(op);
        }
        for (Option op : frameOptions.getOptions()) {
            allOptions.addOption(op);
        }
        allOptions.addOption(apiLevelOption);
        allOptions.addOption(analysisOption);
        allOptions.addOption(debugDecOption);
        allOptions.addOption(noDbgOption);
        allOptions.addOption(forceManOption);
        allOptions.addOption(noAssetOption);
        allOptions.addOption(keepResOption);
        allOptions.addOption(debugBuiOption);
        allOptions.addOption(netSecConfOption);
        allOptions.addOption(aaptOption);
        allOptions.addOption(originalOption);
        allOptions.addOption(verboseOption);
        allOptions.addOption(quietOption);
        allOptions.addOption(aapt2Option);
        allOptions.addOption(noCrunchOption);
        allOptions.addOption(onlyMainClassesOption);
    }

    private static String verbosityHelp() {
        if (isAdvanceMode()) {
            return "[-q|--quiet OR -v|--verbose] ";
        } else {
            return "";
        }
    }

    private static void usage() {
        _Options();
        HelpFormatter formatter = new HelpFormatter();
        formatter.setWidth(120);

        // print out license info prior to formatter.
        System.out.println(
                "Apktool " + ApktoolProperties.getVersion() + " - a tool for reengineering Android apk files\n" +
                        "with smali v" + ApktoolProperties.get("smaliVersion") +
                        " and baksmali v" + ApktoolProperties.get("baksmaliVersion") + "\n" +
                        "Copyright 2010 Ryszard Wiśniewski <brut.alll@gmail.com>\n" +
                        "Copyright 2010 Connor Tumbleson <connor.tumbleson@gmail.com>" );
        if (isAdvanceMode()) {
            System.out.println("Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0)\n");
        }else {
            System.out.println();
        }

        // 4 usage outputs (general, frameworks, decode, build)
        formatter.printHelp("apktool " + verbosityHelp(), normalOptions);
        formatter.printHelp("apktool " + verbosityHelp() + "if|install-framework [options] <framework.apk>", frameOptions);
        formatter.printHelp("apktool " + verbosityHelp() + "d[ecode] [options] <file_apk>", decodeOptions);
        formatter.printHelp("apktool " + verbosityHelp() + "b[uild] [options] <app_path>", buildOptions);
        if (isAdvanceMode()) {
            formatter.printHelp("apktool " + verbosityHelp() + "publicize-resources <file_path>", emptyOptions);
            formatter.printHelp("apktool " + verbosityHelp() + "empty-framework-dir [options]", emptyFrameworkOptions);
            formatter.printHelp("apktool " + verbosityHelp() + "list-frameworks [options]", listFrameworkOptions);
        }
        System.out.println();

        // print out more information
        System.out.println("For additional info, see: https://apktool.org \n"
            + "For smali/baksmali info, see: https://github.com/google/smali");
    }

    private static void setupLogging(final Verbosity verbosity) {
        Logger logger = Logger.getLogger("");
        for (Handler handler : logger.getHandlers()) {
            logger.removeHandler(handler);
        }
        LogManager.getLogManager().reset();

        if (verbosity == Verbosity.QUIET) {
            return;
        }

        Handler handler = new Handler(){
            @Override
            public void publish(LogRecord record) {
                if (getFormatter() == null) {
                    setFormatter(new SimpleFormatter());
                }

                try {
                    String message = getFormatter().format(record);
                    if (record.getLevel().intValue() >= Level.WARNING.intValue()) {
                        System.err.write(message.getBytes());
                    } else {
                        if (record.getLevel().intValue() >= Level.INFO.intValue()) {
                            System.out.write(message.getBytes());
                        } else {
                            if (verbosity == Verbosity.VERBOSE) {
                                System.out.write(message.getBytes());
                            }
                        }
                    }
                } catch (Exception exception) {
                    reportError(null, exception, ErrorManager.FORMAT_FAILURE);
                }
            }
            @Override
            public void close() throws SecurityException {}
            @Override
            public void flush(){}
        };

        logger.addHandler(handler);

        if (verbosity == Verbosity.VERBOSE) {
            handler.setLevel(Level.ALL);
            logger.setLevel(Level.ALL);
        } else {
            handler.setFormatter(new Formatter() {
                @Override
                public String format(LogRecord record) {
                    return record.getLevel().toString().charAt(0) + ": "
                            + record.getMessage()
                            + System.getProperty("line.separator");
                }
            });
        }
    }

    private static boolean isAdvanceMode() {
        return advanceMode;
    }

    private static void setAdvanceMode() {
        Main.advanceMode = true;
    }

    private enum Verbosity {
        NORMAL, VERBOSE, QUIET
    }

    private static boolean advanceMode = false;

    private final static Options normalOptions;
    private final static Options decodeOptions;
    private final static Options buildOptions;
    private final static Options frameOptions;
    private final static Options allOptions;
    private final static Options emptyOptions;
    private final static Options emptyFrameworkOptions;
    private final static Options listFrameworkOptions;

    static {
        //normal and advance usage output
        normalOptions = new Options();
        buildOptions = new Options();
        decodeOptions = new Options();
        frameOptions = new Options();
        allOptions = new Options();
        emptyOptions = new Options();
        emptyFrameworkOptions = new Options();
        listFrameworkOptions = new Options();
    }
}
