package shadow.build.closure;
// needs to be in this package because a bunch of stuff we need is package protected

import com.google.javascript.jscomp.*;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;

import java.util.*;


/**
 * Created by zilence on 21/11/15.
 */
public class ReplaceCLJSConstants implements CompilerPass, NodeTraversal.Callback {

    private final AbstractCompiler compiler;
    private final boolean shadowKeywords;
    private final Map<String, ConstantRef> constants = new HashMap<>();

    public ReplaceCLJSConstants(AbstractCompiler compiler, boolean shadowKeywords) {
        this.compiler = compiler;
        this.shadowKeywords = shadowKeywords;
    }

    @Override
    public void process(Node externs, Node node) {
        if (!constants.isEmpty()) {
            throw new IllegalStateException("can only run once");
        }

        for (CompilerInput input : ShadowAccess.getInputsInOrder(compiler)) {
            // clj/cljs/cljc files only, clj because of self-host macros
            if (input.getName().indexOf(".clj") != -1) {
                NodeTraversal.traverse(compiler, input.getAstRoot(compiler), this);
            }
        }

        for (ConstantRef ref : constants.values()) {
            JSModule targetModule;
            if (ref.usedIn.size() == 1) {
                targetModule = ref.usedIn.iterator().next();
            } else {
                targetModule = ShadowAccess
                        .getModuleGraph(compiler)
                        .getDeepestCommonDependencyInclusive(ref.usedIn);
            }

            // System.out.format("Moving %s to %s (used in %d)\n", ref.fqn, targetModule.getName(), ref.usedIn.size());

            Node target = null;

            for (CompilerInput input : targetModule.getInputs()) {
                if (input.getName().startsWith("shadow/cljs/constants/")) {
                    target = input.getAstRoot(compiler);
                    break;
                }
            }

            if (target == null) {
                throw new IllegalStateException(
                        String.format(
                                "Could not find where to put constant %s. Used by %s, selected common dep %s",
                                ref.varName,
                                ref.usedIn,
                                targetModule.getName()
                        )
                );
            }

            Node constantNode;

            // FIXME: need real life tests if this is actually better
            // calling function + constructing fqn at runtime
            // vs.
            // just calling new with all args, but duplicate ns/name in fqn
            if (shadowKeywords && ref.keyword) {
                // simple keyword
                if (ref.nsNode.isNull()) {
                    constantNode = IR.call(
                            IR.name("shadow$keyword"),
                            ref.nameNode.detach()
                    );
                } else {
                    constantNode = IR.call(
                            IR.name("shadow$keyword_fqn"),
                            ref.nsNode.detach(),
                            ref.nameNode.detach()
                    );
                }
            } else {
                constantNode = ref.node;
            }

            Node varNode = IR.var(IR.name(ref.varName), constantNode);
            target.addChildToBack(varNode);

            ShadowAccess.reportChangeToEnclosingScope(compiler, target);
        }
    }


    public String munge(String sym) {
        // :phone_number and :phone-number munge to the same output
        // replace - first so it doesn't get munged to _
        // needs to replace dots as well
        return clojure.lang.Compiler.munge(sym.replaceAll("-", "_DASH_")).replaceAll("\\.", "_DOT_");
    }

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

    public void visit(NodeTraversal t, Node n, Node parent) {
        // new cljs.core.Keyword(ns, name, fqn, hash);
        // new cljs.core.Symbol(ns, name, fqn, hash, meta);

        if (n.isNew()) {
            int childCount = n.getChildCount();

            // cljs.core.Keyword NOT new something()
            // must check isGetProp, new something['whatever']() blows up getQualifiedName()
            // getprop, ns, name, fqn, hash (keyword), 5 nodes
            // getprop, ns, name, fqn, hash, meta (symbol), 6 nodes, if meta is not null don't replace the symbol
            if (n.getFirstChild().isGetProp() && (childCount == 5 || (childCount == 6 && n.getChildAtIndex(5).isNull()))) {
                String typeName = n.getFirstChild().getQualifiedName();

                if (typeName.equals("cljs.core.Keyword") || typeName.equals("cljs.core.Symbol")) {
                    final Node nsNode = n.getChildAtIndex(1);
                    final Node nameNode = n.getChildAtIndex(2);
                    final Node fqnNode = n.getChildAtIndex(3);
                    final Node hashNode = n.getChildAtIndex(4);

                    if ((nsNode.isString() || nsNode.isNull()) // ns may be null
                            && nameNode.isString() // name is never null
                            && fqnNode.isString() // fqn is never null
                            && hashNode.isNumber()) { // hash is precomputed

                        String fqn = fqnNode.getString();
                        String constantName = "cljs$cst$" + typeName.substring(typeName.lastIndexOf(".") + 1).toLowerCase() + "$" + munge(fqn);

                        ConstantRef ref = constants.get(constantName);
                        if (ref == null) {
                            ref = new ConstantRef(constantName, typeName.equals("cljs.core.Keyword"), n, nsNode, nameNode, hashNode);
                            constants.put(constantName, ref);
                        }
                        ref.usedIn.add(t.getModule());
                        // new versions use a bitset
                        ref.usedInBits.set(t.getModule().getIndex());

                        parent.replaceChild(n, IR.name(constantName));
                        ShadowAccess.reportChangeToEnclosingScope(compiler, parent);
                    }
                }
            }
        }
    }

    public class ConstantRef {
        final String varName;
        final boolean keyword;
        final Node node;
        final Node nsNode;
        final Node nameNode;
        final Node hashNode;
        Set<JSModule> usedIn;
        BitSet usedInBits;

        public ConstantRef(String varName, boolean keyword, Node node, Node nsNode, Node nameNode, Node hashNode) {
            this.varName = varName;
            this.keyword = keyword;
            this.node = node;
            this.nsNode = nsNode;
            this.nameNode = nameNode;
            this.hashNode = hashNode;
            this.usedIn = new HashSet<>();
            this.usedInBits = new BitSet();
        }
    }
}

@@ -148,16 +148,62 @@
 comp.*;%0A
+import com.google.javascript.jscomp.Compiler;%0A
 import c
@@ -443,24 +443,16 @@
 e final 
-Abstract
 Compiler
@@ -613,16 +613,8 @@
 nts(
-Abstract
 Comp
@@ -2061,334 +2061,472 @@
     
-if (target == null) %7B%0A                throw new IllegalStateException(%0A                        String.format(%0A                                %22Could not find where to put constant %25s. Used by %25s, selected common dep %25s%22,%0A                                ref.varName,%0A                                ref.usedIn,%0A                
+// sometimes the closure-compiler selects a shared module that isn't cljs%0A            // and doesn't have the constants input to place things in%0A            // in that case just append to cljs.core things that is guaranteed to be shared among all%0A            // mostly affects :npm-module since it creates quite a few modules%0A            // not a big deal if things get placed in cljs.core, closure might move it on its own again%0A            if (target == null) %7B%0A
     
@@ -2547,67 +2547,50 @@
 rget
-Modu
+ = compi
 le
+r
 .get
-Name()%0A                        )%0A                
+ScriptNode(%22cljs/core.cljs%22
 );%0A 

fix IllegalStateException for :npm-module builds