/*
 * Copyright (c) 2013 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.cast.js.html;

import com.ibm.wala.cast.ir.translator.TranslatorToIR;
import com.ibm.wala.cast.js.loader.JavaScriptLoader;
import com.ibm.wala.cast.js.loader.JavaScriptLoaderFactory;
import com.ibm.wala.cast.js.ssa.JSInstructionFactory;
import com.ibm.wala.cast.js.translator.JSAstTranslator;
import com.ibm.wala.cast.js.translator.JavaScriptTranslatorFactory;
import com.ibm.wala.cast.js.types.JavaScriptTypes;
import com.ibm.wala.cast.tree.CAst;
import com.ibm.wala.cast.tree.CAstEntity;
import com.ibm.wala.cast.tree.CAstNode;
import com.ibm.wala.cast.tree.impl.CAstImpl;
import com.ibm.wala.cast.tree.impl.CAstOperator;
import com.ibm.wala.cast.tree.rewrite.CAstRewriterFactory;
import com.ibm.wala.cast.tree.visit.CAstVisitor;
import com.ibm.wala.classLoader.IClassLoader;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.types.TypeReference;

public class WebPageLoaderFactory extends JavaScriptLoaderFactory {

  public WebPageLoaderFactory(JavaScriptTranslatorFactory factory) {
    super(factory);
  }

  public WebPageLoaderFactory(
      JavaScriptTranslatorFactory factory, CAstRewriterFactory<?, ?> preprocessor) {
    super(factory, preprocessor);
  }

  @Override
  protected IClassLoader makeTheLoader(IClassHierarchy cha) {
    return new JavaScriptLoader(cha, translatorFactory, preprocessor) {
      @Override
      protected TranslatorToIR initTranslator() {
        return new JSAstTranslator(this) {
          private final CAst Ast = new CAstImpl();

          private boolean isNestedWithinScriptBody(WalkContext context) {
            return isScriptBody(context) || context.getName().contains("__WINDOW_MAIN__");
          }

          private boolean isScriptBody(WalkContext context) {
            return context.top().getName().equals("__WINDOW_MAIN__");
          }

          @Override
          protected int doGlobalRead(
              CAstNode n, WalkContext context, String name, TypeReference type) {
            int result = context.currentScope().allocateTempValue();
            if (isNestedWithinScriptBody(context)
                && !"$$undefined".equals(name)
                && !"window".equals(name)
                && !name.startsWith("$$destructure")) {

              // check if field is defined on 'window'
              int windowVal =
                  isScriptBody(context)
                      ? super.doLocalRead(context, "this", JavaScriptTypes.Root)
                      : super.doGlobalRead(n, context, "window", type);
              int isDefined = context.currentScope().allocateTempValue();
              context.currentScope().getConstantValue(name);
              doIsFieldDefined(context, isDefined, windowVal, Ast.makeConstant(name));
              context
                  .cfg()
                  .addInstruction(
                      insts.ConditionalBranchInstruction(
                          context.cfg().getCurrentInstruction(),
                          translateConditionOpcode(CAstOperator.OP_NE),
                          null,
                          isDefined,
                          context.currentScope().getConstantValue(0),
                          -1));
              PreBasicBlock srcB = context.cfg().getCurrentBlock();

              // field lookup of value
              context.cfg().newBlock(true);
              context
                  .cfg()
                  .addInstruction(
                      ((JSInstructionFactory) insts)
                          .GetInstruction(
                              context.cfg().getCurrentInstruction(), result, windowVal, name));
              context.cfg().newBlock(true);
              context
                  .cfg()
                  .addInstruction(insts.GotoInstruction(context.cfg().getCurrentInstruction(), -1));
              PreBasicBlock trueB = context.cfg().getCurrentBlock();

              // read global
              context.cfg().newBlock(false);
              PreBasicBlock falseB = context.cfg().getCurrentBlock();
              int sr = super.doGlobalRead(n, context, name, type);
              context
                  .cfg()
                  .addInstruction(
                      ((JSInstructionFactory) insts)
                          .AssignInstruction(context.cfg().getCurrentInstruction(), result, sr));

              // end
              context.cfg().newBlock(true);

              context.cfg().addEdge(trueB, context.cfg().getCurrentBlock());
              context.cfg().addEdge(srcB, falseB);

              return result;

            } else {
              return super.doGlobalRead(n, context, name, type);
            }
          }

          @Override
          protected void doLocalWrite(
              WalkContext context, String nm, TypeReference type, int rval) {
            if (isNestedWithinScriptBody(context)
                && !"$$undefined".equals(nm)
                && !"window".equals(nm)
                && !nm.startsWith("$$destructure")) {
              int windowVal = super.doLocalRead(context, "this", type);
              context.currentScope().getConstantValue(nm);
              context
                  .cfg()
                  .addInstruction(
                      ((JSInstructionFactory) insts)
                          .PutInstruction(
                              context.cfg().getCurrentInstruction(), windowVal, rval, nm));
            }

            super.doLocalWrite(context, nm, type, rval);
          }

          @Override
          protected void leaveFunctionStmt(
              CAstNode n, WalkContext context, CAstVisitor<WalkContext> visitor) {
            super.leaveFunctionStmt(n, context, visitor);
            if (isScriptBody(context)) {
              CAstEntity fn = (CAstEntity) n.getChild(0).getValue();
              int fnValue = context.currentScope().lookup(fn.getName()).valueNumber();
              assert fnValue > 0;
              int windowVal = super.doLocalRead(context, "this", JavaScriptTypes.Function);
              context
                  .cfg()
                  .addInstruction(
                      ((JSInstructionFactory) insts)
                          .PutInstruction(
                              context.cfg().getCurrentInstruction(),
                              windowVal,
                              fnValue,
                              fn.getName()));
            }
          }
        };
      }
    };
  }
}
