/*
 * Copyright (c) 2015 Justin Garrick
 *
 * 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.justingarrick.reverser;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseException;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.BodyDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import com.justingarrick.reverser.cli.Settings;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Reverses the R.java mappings generated by decompilers
 * in code so that decompiled/deobfuscated application
 * code is more readable.
 */
public class Reverser {

    /** Settings parsed from command line. */
    private final Settings settings;

    /** A map of R.java ids to fully-qualified strings, e.g. 2130968581 -> "R.drawable.ic_launcher". */
    private final Map<Integer, String> transform = new HashMap<>();

    /**
     * Main entry point for the application.
     *
     * @param args command line arguments
     */
    public static void main(String... args) {
        Reverser reverser = new Reverser(args);
        reverser.reverse();
    }

    /**
     * Create a new reverser.
     *
     * @param args command line arguments
     */
    public Reverser(String... args) {
        settings = parseCli(args);
    }

    /**
     * Reverse the mappings corresponding to the command line arguments passed.
     */
    public void reverse() {
        SourceSet sourceSet = new SourceSet(settings.getSource());
        Set<Path> javaFiles = sourceSet.getJavaFiles(settings.getPackage());
        Set<Path> rFiles = sourceSet.getRFiles(settings.getPackage());
        javaFiles.removeAll(rFiles);
        createTransform(rFiles);
        applyTransform(javaFiles);
    }

    /**
     * Parse command line args into an object.
     *
     * @param args raw command line arguments
     * @return a populated settings object
     */
    private Settings parseCli(String... args) {
        Settings settings = new Settings();
        JCommander commander = new JCommander(settings);
        try {
            commander.parse(args);
        } catch (ParameterException e) {
            System.err.println(e.getMessage());
            commander.usage();
            System.exit(0);
        }
        return settings;
    }

    /**
     * Parses the R.java file(s) and creates a mapping of ids to fully qualified strings.
     *
     * @param rFiles the set of r.java files
     */
    private void createTransform(Set<Path> rFiles) {
        ClassVisitor visitor = new ClassVisitor();
        rFiles.stream().forEach(path -> {
            try {
                CompilationUnit unit = JavaParser.parse(path.toFile());
                visitor.visit(unit, null);
            } catch (ParseException | IOException e) {
                System.err.println("Unable to parse " + path.getFileName() + ": " + e.getMessage());
            }
        });
    }

    /**
     * Applies the previously generated mapping to non-R.java source files to increase readability.
     *
     * @param javaFiles the set of .java files with the r.java subset removed
     */
    private void applyTransform(Set<Path> javaFiles) {
        javaFiles.stream()
                .forEach(path -> {
                    try {
                        String fileAsString = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
                        for (Map.Entry<Integer, String> entry : transform.entrySet()) {
                            fileAsString = fileAsString.replaceAll(entry.getKey().toString(), entry.getValue());
                        }
                        Files.write(path, fileAsString.getBytes(StandardCharsets.UTF_8));
                    } catch (IOException e) {
                        System.err.println("Failed to read/write " + path.getFileName());
                    }
                });
    }

    /**
     * JavaParser visitor that recursively visits the classes, subclasses, and members in R.java file(s)
     */
    private class ClassVisitor extends VoidVisitorAdapter {
        @Override
        public void visit(ClassOrInterfaceDeclaration clazz, Object arg) {
            for (BodyDeclaration member : clazz.getMembers()) {
                if (member instanceof ClassOrInterfaceDeclaration)
                    visit((ClassOrInterfaceDeclaration)member, arg);
                else if (member instanceof FieldDeclaration) {
                    FieldDeclaration field = (FieldDeclaration)member;
                    String type = null != field.getType() ? field.getType().toString() : "";
                    if (type.equals("int")) {
                        VariableDeclarator variable = field.getVariables().stream().findFirst().get();
                        String name = variable.getId().toString();
                        Integer value = null != variable.getInit() ? Integer.parseInt(variable.getInit().toString()) : 0;
                        // decimal value of 0x7f000000, which is what AAPT starts numbering at - https://stackoverflow.com/questions/6517151/how-does-the-mapping-between-android-resources-and-resources-id-work/6646113#6646113
                        if (value >= 2130706432) {
                            name = "R." + ((ClassOrInterfaceDeclaration)field.getParentNode()).getName() + "." + name;
                            transform.put(value, name);
                        }
                    }
                }
            }
        }
    }

}