package shadow.build.closure;

import com.google.javascript.jscomp.*;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.TokenUtil;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class PropertyCollector implements NodeTraversal.Callback, CompilerPass {
    private final AbstractCompiler compiler;
    public final Map<String, Set<String>> properties = new HashMap<>();

    public PropertyCollector(AbstractCompiler compiler) {
        this.compiler = compiler;
    }

    @Override
    public boolean shouldTraverse(NodeTraversal t, Node node, Node parent) {
        return true;
    }

    // TokenUtil.isJSIdentifier is package protected ...
    public static boolean isJSIdentifier(String s) {
        int length = s.length();
        if (length != 0 && !Character.isIdentifierIgnorable(s.charAt(0)) && Character.isJavaIdentifierStart(s.charAt(0))) {
            for (int i = 1; i < length; ++i) {
                if (Character.isIdentifierIgnorable(s.charAt(i)) || !Character.isJavaIdentifierPart(s.charAt(i))) {
                    return false;
                }
            }

            return true;
        } else {
            return false;
        }
    }

    public final static Set<String> ignoredProps = new HashSet<>();

    static {
        ignoredProps.add("exports");
        ignoredProps.add("module");
        ignoredProps.add("prototype");
    }

    private void addProperty(NodeTraversal t, String property) {
        if (!property.equals("exports") && !property.equals("module") && isJSIdentifier(property)) {
            String sourceName = t.getSourceName();
            Set<String> x = properties.get(sourceName);
            if (x == null) {
                x = new HashSet<>();
                properties.put(sourceName, x);
            }
            x.add(property);
        }
    }

    @Override
    public void visit(NodeTraversal t, Node node, Node parent) {
        // exports.foo = 1;
        // function Foo() {}
        // Foo.prototype.bar = 1;
        // exports.Foo = Foo;
        // find every property assign
        // extract property name
        // use as :externs in :advanced mode build using JS deps
        // profit?
        // FIXME: bad idea? probably way too many names we don't actually need to preserve
        if (node.isAssign()) {
            Node getProp = node.getFirstChild();
            // only collect assignments to props
            // thing.x = 1;
            // not
            // var x = 1;
            // var x; x = 1;
            if (getProp.isGetProp()) {
                Node name = getProp.getLastChild();
                if (!name.isString()) {
                    System.out.println("==== assign without string");
                    System.out.println(node.toStringTree());
                    System.out.println("====");
                } else {
                    String property = name.getString();
                    addProperty(t, property);
                }
            } else {
                // FIXME: other cases I'm missing?
                // System.out.println("===== assign without getprop");
                // System.out.println(node.toStringTree());
                // System.out.println("=====");
            }
        } else if (node.isObjectLit()) {
            for (int i = 0; i < node.getChildCount(); i++) {
                Node keyNode = node.getChildAtIndex(i);
                if (keyNode.isStringKey() || keyNode.isGetterDef() || keyNode.isSetterDef()) {
                    addProperty(t, keyNode.getString());
                } else {
                    System.out.println("==== object-lit without string key");
                    System.out.println(node.toStringTree());
                }
            }
        } else if (NodeUtil.isCallTo(node,"Object.defineProperty")) {
            Node property = node.getChildAtIndex(2);
            if (property.isString()) {
                addProperty(t, property.getString());
            }
        }
    }

    public void process(Node externs, Node root) {
        NodeTraversal.traverseEs6(compiler, root, this);
    }

    public static Node process(Compiler cc, SourceFile srcFile) {
        JsAst ast = new JsAst(srcFile);
        Node node = ast.getAstRoot(cc);

        JsAst.ParseResult result = (JsAst.ParseResult) node.getProp(Node.PARSE_RESULTS);

        PropertyCollector pass = new PropertyCollector(cc);
        NodeTraversal.traverseEs6(cc, node, pass);


        System.out.println(pass.properties);

        return node;
    }

    public static void main(String... args) {
        Compiler cc = new Compiler();

        CompilerOptions co = new CompilerOptions();
        co.setLanguageIn(CompilerOptions.LanguageMode.ECMASCRIPT_2017);
        co.setPrettyPrint(true);
        cc.initOptions(co);

        // SourceFile srcFile = SourceFile.fromFile("node_modules/@firebase/util/dist/cjs/src/crypt.js");
        SourceFile srcFile = SourceFile.fromFile("test/closure-inputs/assigns.js");
        cc.toSource(process(cc, srcFile));

    }
}

@@ -707,24 +707,608 @@
 rue;%0A    %7D%0A%0A
+    // private in parser/Scanner.java%0A    public static boolean isIdentifierStart(char ch) %7B%0A        switch (ch) %7B%0A            case '$':%0A            case '_':%0A                return true;%0A            default:%0A                // Workaround b/36459436%0A                // When running under GWT, Character.isLetter only handles ASCII%0A                // Angular relies heavily on U+0275 (Latin Barred O)%0A                return ch == 0x0275%0A                        // TODO: UnicodeLetter also includes Letter Number (NI)%0A                        %7C%7C Character.isLetter(ch);%0A        %7D%0A    %7D%0A%0A
     // Token
@@ -1469,73 +1469,10 @@
  && 
-!Character.isIdentifierIgnorable(s.charAt(0)) && Character.isJava
+is
 Iden

fix externs generator picking up invalid characters