/**
   * Creates a vm program. If the given file is a dir, creates a program composed of the vm files in
   * the dir. The vm files are scanned twice: in the first scan a symbol table (that maps function &
   * label names into addresses) is built. In the second scan, the instructions array is built.
   * Throws ProgramException if an error occurs while loading the program.
   */
  public void loadProgram(String fileName) throws ProgramException {
    File file = new File(fileName);
    if (!file.exists()) throw new ProgramException("cannot find " + fileName);

    File[] files;

    if (file.isDirectory()) {
      files = file.listFiles(new HackFileFilter(".vm"));
      if (files == null || files.length == 0)
        throw new ProgramException("No vm files found in " + fileName);
    } else files = new File[] {file};

    if (displayChanges) gui.showMessage("Loading...");

    // First scan
    staticRange.clear();
    functions.clear();
    builtInAccessStatus = BUILTIN_ACCESS_UNDECIDED;
    Hashtable symbols = new Hashtable();
    nextPC = 0;
    for (int i = 0; i < files.length; i++) {
      String name = files[i].getName();
      String className = name.substring(0, name.indexOf("."));
      // put some dummy into static range - just to tell the function
      // getAddress in the second pass which classes exist
      staticRange.put(className, new Boolean(true));
      try {
        updateSymbolTable(files[i], symbols, functions);
      } catch (ProgramException pe) {
        if (displayChanges) gui.hideMessage();
        throw new ProgramException(name + ": " + pe.getMessage());
      }
    }
    boolean addCallBuiltInSysInit = false;
    if ((file.isDirectory() || symbols.get("Main.main") != null)
        && symbols.get("Sys.init") == null) {
      // If the program is in multiple files or there's a Main.main
      // function it is assumed that it should be run by calling Sys.init.
      // If no Sys.init is found, add an invisible line with a call
      // to Sys.init to start on - the builtin version will be called.
      addCallBuiltInSysInit = true;
      getAddress("Sys.init"); // confirm calling the built-in Sys.init
      ++nextPC; // A "call Sys.init 0" line will be added
    }

    instructions = new VMEmulatorInstruction[nextPC + 4];

    // Second scan
    nextPC = 0;
    currentStaticIndex = Definitions.VAR_START_ADDRESS;
    for (int i = 0; i < files.length; i++) {
      String name = files[i].getName();
      String className = name.substring(0, name.indexOf("."));

      largestStaticIndex = -1;
      int[] range = new int[2];
      range[0] = currentStaticIndex;

      try {
        // functions is not passed as an argument since it is accessed
        // through getAddress()
        buildProgram(files[i], symbols);
      } catch (ProgramException pe) {
        if (displayChanges) gui.hideMessage();
        throw new ProgramException(name + ": " + pe.getMessage());
      }

      currentStaticIndex += largestStaticIndex + 1;
      range[1] = currentStaticIndex - 1;
      staticRange.put(className, range);
    }
    instructionsLength = visibleInstructionsLength = nextPC;
    if (builtInAccessStatus == BUILTIN_ACCESS_AUTHORIZED) {
      // Add some "invisible" code in the end to make everything work
      instructionsLength += 4;
      if (addCallBuiltInSysInit) {
        instructionsLength += 1;
      }
      short indexInInvisibleCode = 0;
      // Add a jump to the end (noone should get here since
      // both calls to built-in functions indicate that
      // that this is a function-based program and not a script
      // a-la proj7, but just to be on the safe side...).
      instructions[nextPC] =
          new VMEmulatorInstruction(
              HVMInstructionSet.GOTO_CODE, (short) instructionsLength, indexInInvisibleCode);
      instructions[nextPC].setStringArg("afterInvisibleCode");
      nextPC++;
      // Add a small infinite loop for built-in
      // methods to call (for example when Sys.halt is
      // called it must call a non-built-in infinite loop
      // because otherwise the current script would not
      // finish running - a problem for the OS tests.
      instructions[nextPC] = new VMEmulatorInstruction(HVMInstructionSet.LABEL_CODE, (short) -1);
      instructions[nextPC].setStringArg("infiniteLoopForBuiltIns");
      nextPC++;
      infiniteLoopForBuiltInsAddress = nextPC;
      instructions[nextPC] =
          new VMEmulatorInstruction(HVMInstructionSet.GOTO_CODE, nextPC, ++indexInInvisibleCode);
      instructions[nextPC].setStringArg("infiniteLoopForBuiltIns");
      nextPC++;
      if (addCallBuiltInSysInit) { // Add a call to the built-in Sys.init
        instructions[nextPC] =
            new VMEmulatorInstruction(
                HVMInstructionSet.CALL_CODE,
                getAddress("Sys.init"),
                (short) 0,
                ++indexInInvisibleCode);
        instructions[nextPC].setStringArg("Sys.init");
        startAddress = nextPC;
        nextPC++;
      }
      // Add the label that the first invisible code line jumps to
      instructions[nextPC] = new VMEmulatorInstruction(HVMInstructionSet.LABEL_CODE, (short) -1);
      instructions[nextPC].setStringArg("afterInvisibleCode");
      nextPC++;
    }

    if (!addCallBuiltInSysInit) {
      Short sysInitAddress = (Short) symbols.get("Sys.init");
      if (sysInitAddress == null) // Single file, no Sys.init - start at 0
      startAddress = 0;
      else // Implemented Sys.init - start there
      startAddress = sysInitAddress.shortValue();
    }

    if (displayChanges) gui.hideMessage();

    nextPC = startAddress;
    setGUIContents();

    notifyProgramListeners(ProgramEvent.LOAD, fileName);
  }