@Override
  public void build(CompileContext context) throws ProjectBuildException {
    if (!AndroidJpsUtil.containsAndroidFacet(context.getProject())
        || AndroidJpsUtil.isLightBuild(context)) {
      return;
    }
    final Collection<Module> modules = context.getProject().getModules().values();
    final Map<Module, AndroidFileSetState> resourcesStates =
        new HashMap<Module, AndroidFileSetState>();
    final Map<Module, AndroidFileSetState> assetsStates =
        new HashMap<Module, AndroidFileSetState>();

    try {
      fillStates(modules, resourcesStates, assetsStates);

      if (!doCaching(context, modules, resourcesStates)) {
        throw new ProjectBuildException();
      }

      if (!doResourcePackaging(context, modules, resourcesStates, assetsStates)) {
        throw new ProjectBuildException();
      }

      if (!doPackaging(context, modules)) {
        throw new ProjectBuildException();
      }
    } catch (ProjectBuildException e) {
      throw e;
    } catch (Exception e) {
      AndroidJpsUtil.handleException(context, e, BUILDER_NAME);
    }
  }
  @SuppressWarnings("unchecked")
  private static AndroidFileSetState buildCurrentApkBuilderState(
      @NotNull Project project,
      @NotNull String resPackagePath,
      @NotNull String classesDexFilePath,
      @NotNull String[] nativeLibDirs,
      @NotNull String[] sourceRoots,
      @NotNull String[] externalJars,
      boolean release) {
    final List<String> roots = new ArrayList<String>();
    roots.add(resPackagePath);
    roots.add(classesDexFilePath);
    roots.addAll(Arrays.asList(externalJars));

    for (String sourceRootPath : sourceRoots) {
      final List<File> files = new ArrayList<File>();
      AndroidApkBuilder.collectStandardSourceFolderResources(
          new File(sourceRootPath), files, new MyExcludedSourcesFilter(project));
      roots.addAll(AndroidJpsUtil.toPaths(files));
    }

    for (String nativeLibDir : nativeLibDirs) {
      final List<File> files = new ArrayList<File>();
      AndroidApkBuilder.collectNativeLibraries(new File(nativeLibDir), files, !release);
      roots.addAll(AndroidJpsUtil.toPaths(files));
    }

    return new AndroidFileSetState(roots, Condition.TRUE, false);
  }
  private static boolean runPngCaching(
      @NotNull CompileContext context,
      @NotNull Module module,
      @NotNull AndroidFileSetStorage storage,
      @Nullable AndroidFileSetState state)
      throws IOException {
    final AndroidFileSetState savedState = storage.getState(module.getName());
    if (context.isMake() && savedState != null && savedState.equalsTo(state)) {
      return true;
    }

    final AndroidFacet facet = AndroidJpsUtil.getFacet(module);
    if (facet == null) {
      return true;
    }

    context.processMessage(
        new CompilerMessage(
            BUILDER_NAME,
            BuildMessage.Kind.INFO,
            AndroidJpsBundle.message("android.jps.progress.res.caching", module.getName())));

    final File resourceDir = AndroidJpsUtil.getResourceDirForCompilationPath(facet);
    if (resourceDir == null) {
      return true;
    }

    final Pair<AndroidSdk, IAndroidTarget> pair =
        AndroidJpsUtil.getAndroidPlatform(module, context, BUILDER_NAME);
    if (pair == null) {
      return false;
    }

    final File resCacheDir = AndroidJpsUtil.getResourcesCacheDir(context, module);

    if (!resCacheDir.exists()) {
      if (!resCacheDir.mkdirs()) {
        context.processMessage(
            new CompilerMessage(
                BUILDER_NAME,
                BuildMessage.Kind.ERROR,
                "Cannot create directory " + resCacheDir.getPath()));
        return false;
      }
    }

    final IAndroidTarget target = pair.second;

    final Map<AndroidCompilerMessageKind, List<String>> messages =
        AndroidApt.crunch(
            target, Collections.singletonList(resourceDir.getPath()), resCacheDir.getPath());

    AndroidJpsUtil.addMessages(context, messages, BUILDER_NAME);

    final boolean success = messages.get(AndroidCompilerMessageKind.ERROR).isEmpty();
    storage.update(module.getName(), success ? state : null);
    return success;
  }
  @SuppressWarnings("unchecked")
  private static void fillStates(
      @NotNull Collection<Module> modules,
      @NotNull Map<Module, AndroidFileSetState> resourcesStates,
      @NotNull Map<Module, AndroidFileSetState> assetsStates)
      throws IOException {
    for (Module module : modules) {
      final AndroidFacet facet = AndroidJpsUtil.getFacet(module);

      if (facet != null) {
        final File resourceDir = facet.getResourceDir();
        final List<String> resourceDirs =
            resourceDir != null
                ? Arrays.asList(resourceDir.getPath())
                : Collections.<String>emptyList();
        resourcesStates.put(module, new AndroidFileSetState(resourceDirs, Condition.TRUE, true));

        final File assetsDir = facet.getAssetsDir();
        final List<String> assetDirs =
            assetsDir != null
                ? Arrays.asList(assetsDir.getPath())
                : Collections.<String>emptyList();
        assetsStates.put(module, new AndroidFileSetState(assetDirs, Condition.TRUE, true));
      }
    }
  }
  private static boolean doCaching(
      @NotNull CompileContext context,
      @NotNull Collection<Module> modules,
      @NotNull Map<Module, AndroidFileSetState> module2state)
      throws IOException {
    boolean success = true;
    final File dataStorageRoot = context.getDataManager().getDataStorageRoot();
    final AndroidFileSetStorage storage =
        new AndroidFileSetStorage(dataStorageRoot, "resource_caching");

    try {
      for (Module module : modules) {
        final AndroidFileSetState state = module2state.get(module);

        try {
          if (!runPngCaching(context, module, storage, state)) {
            success = false;
          }
        } catch (IOException e) {
          AndroidJpsUtil.reportExceptionError(context, null, e, BUILDER_NAME);
        }
      }
    } finally {
      storage.close();
    }
    return success;
  }
  private static boolean doPackaging(
      @NotNull CompileContext context, @NotNull Collection<Module> modules) throws IOException {
    final boolean release = AndroidJpsUtil.isReleaseBuild(context);
    final File dataStorageRoot = context.getDataManager().getDataStorageRoot();

    boolean success = true;

    AndroidFileSetStorage apkFileSetStorage = null;
    AndroidApkBuilderConfigStateStorage apkBuilderConfigStateStorage = null;
    try {
      final String apkFileSetStorageName = "apk_builder_file_set" + (release ? "_release" : "_dev");
      apkFileSetStorage = new AndroidFileSetStorage(dataStorageRoot, apkFileSetStorageName);

      final String apkBuilderStateStorageName =
          "apk_builder_config" + (release ? "_release" : "_dev");
      apkBuilderConfigStateStorage =
          new AndroidApkBuilderConfigStateStorage(dataStorageRoot, apkBuilderStateStorageName);

      for (Module module : modules) {
        try {
          if (!doPackagingForModule(
              context, module, apkFileSetStorage, apkBuilderConfigStateStorage, release)) {
            success = false;
          }
        } catch (IOException e) {
          AndroidJpsUtil.reportExceptionError(context, null, e, BUILDER_NAME);
          success = false;
        }
      }
    } finally {
      if (apkFileSetStorage != null) {
        apkFileSetStorage.close();
      }

      if (apkBuilderConfigStateStorage != null) {
        apkBuilderConfigStateStorage.close();
      }
    }

    return success;
  }
  private static boolean packageResources(
      @NotNull AndroidFacet facet, @NotNull CompileContext context) {
    final Module module = facet.getModule();

    try {
      context.processMessage(
          new ProgressMessage(
              AndroidJpsBundle.message(
                  "android.jps.progress.packaging.resources", module.getName())));

      final File manifestFile = AndroidJpsUtil.getManifestFileForCompilationPath(facet);
      if (manifestFile == null) {
        context.processMessage(
            new CompilerMessage(
                BUILDER_NAME,
                BuildMessage.Kind.ERROR,
                AndroidJpsBundle.message(
                    "android.jps.errors.manifest.not.found", module.getName())));
        return false;
      }
      final ArrayList<String> assetsDirPaths = new ArrayList<String>();
      collectAssetDirs(facet, assetsDirPaths);

      final File outputDir =
          AndroidJpsUtil.getOutputDirectoryForPackagedFiles(context.getProjectPaths(), module);
      if (outputDir == null) {
        context.processMessage(
            new CompilerMessage(
                BUILDER_NAME,
                BuildMessage.Kind.ERROR,
                AndroidJpsBundle.message(
                    "android.jps.errors.output.dir.not.specified", module.getName())));
        return false;
      }

      final Pair<AndroidSdk, IAndroidTarget> pair =
          AndroidJpsUtil.getAndroidPlatform(module, context, BUILDER_NAME);
      if (pair == null) {
        return false;
      }
      final IAndroidTarget target = pair.getSecond();

      final String outputFilePath = getPackagedResourcesFile(module, outputDir).getPath();
      final String[] resourceDirPaths =
          AndroidJpsUtil.collectResourceDirsForCompilation(facet, true, context);

      return doPackageResources(
          context,
          manifestFile,
          target,
          resourceDirPaths,
          ArrayUtil.toStringArray(assetsDirPaths),
          outputFilePath,
          AndroidJpsUtil.isReleaseBuild(context));
    } catch (IOException e) {
      AndroidJpsUtil.reportExceptionError(context, null, e, BUILDER_NAME);
      return false;
    }
  }
  private static boolean doPackageResources(
      @NotNull final CompileContext context,
      @NotNull File manifestFile,
      @NotNull IAndroidTarget target,
      @NotNull String[] resourceDirPaths,
      @NotNull String[] assetsDirPaths,
      @NotNull String outputFilePath,
      boolean releasePackage) {
    try {
      final String outputPath = releasePackage ? outputFilePath + RELEASE_SUFFIX : outputFilePath;

      final IgnoredFilePatterns ignoredFilePatterns = context.getProject().getIgnoredFilePatterns();

      final Map<AndroidCompilerMessageKind, List<String>> messages =
          AndroidApt.packageResources(
              target,
              -1,
              manifestFile.getPath(),
              resourceDirPaths,
              assetsDirPaths,
              outputPath,
              null,
              !releasePackage,
              0,
              new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                  return !ignoredFilePatterns.isIgnored(PathUtil.getFileName(pathname.getPath()));
                }
              });

      AndroidJpsUtil.addMessages(context, messages, BUILDER_NAME);
      return messages.get(AndroidCompilerMessageKind.ERROR).size() == 0;
    } catch (final IOException e) {
      AndroidJpsUtil.reportExceptionError(context, null, e, BUILDER_NAME);
      return false;
    }
  }
  private static void collectAssetDirs(@NotNull AndroidFacet facet, @NotNull List<String> result)
      throws IOException {
    final File assetsDir = facet.getAssetsDir();

    if (assetsDir != null) {
      result.add(assetsDir.getPath());
    }

    for (AndroidFacet depFacet :
        AndroidJpsUtil.getAllAndroidDependencies(facet.getModule(), true)) {
      final File depAssetsDir = depFacet.getAssetsDir();

      if (depAssetsDir != null) {
        result.add(depAssetsDir.getPath());
      }
    }
  }
  @NotNull
  private static String[] collectNativeLibsFolders(@NotNull AndroidFacet facet) throws IOException {
    final List<String> result = new ArrayList<String>();
    final File libsDir = facet.getNativeLibsDir();

    if (libsDir != null) {
      result.add(libsDir.getPath());
    }

    for (AndroidFacet depFacet :
        AndroidJpsUtil.getAllAndroidDependencies(facet.getModule(), true)) {
      final File depLibsDir = depFacet.getNativeLibsDir();
      if (depLibsDir != null) {
        result.add(depLibsDir.getPath());
      }
    }
    return ArrayUtil.toStringArray(result);
  }
  private static boolean checkUpToDate(
      @NotNull Module module,
      @NotNull Map<Module, AndroidFileSetState> module2state,
      @NotNull AndroidFileSetStorage storage)
      throws IOException {
    final AndroidFileSetState moduleState = module2state.get(module);
    final AndroidFileSetState savedState = storage.getState(module.getName());
    if (savedState == null || !savedState.equalsTo(moduleState)) {
      return false;
    }

    for (AndroidFacet libFacet : AndroidJpsUtil.getAllAndroidDependencies(module, true)) {
      final Module libModule = libFacet.getModule();
      final AndroidFileSetState currentLibState = module2state.get(libModule);
      final AndroidFileSetState savedLibState = storage.getState(libModule.getName());

      if (savedLibState == null || !savedLibState.equalsTo(currentLibState)) {
        return false;
      }
    }
    return true;
  }
  private static boolean doPackagingForModule(
      @NotNull CompileContext context,
      @NotNull Module module,
      @NotNull AndroidFileSetStorage apkFileSetStorage,
      @NotNull AndroidApkBuilderConfigStateStorage apkBuilderConfigStateStorage,
      boolean release)
      throws IOException {
    final AndroidFacet facet = AndroidJpsUtil.getFacet(module);
    if (facet == null || facet.isLibrary()) {
      return true;
    }

    final String[] sourceRoots =
        AndroidJpsUtil.toPaths(AndroidJpsUtil.getSourceRootsForModuleAndDependencies(module));
    final ProjectPaths paths = context.getProjectPaths();

    final File outputDir = AndroidJpsUtil.getOutputDirectoryForPackagedFiles(paths, module);
    if (outputDir == null) {
      context.processMessage(
          new CompilerMessage(
              BUILDER_NAME,
              BuildMessage.Kind.ERROR,
              AndroidJpsBundle.message(
                  "android.jps.errors.output.dir.not.specified", module.getName())));
      return false;
    }

    final Pair<AndroidSdk, IAndroidTarget> pair =
        AndroidJpsUtil.getAndroidPlatform(module, context, BUILDER_NAME);
    if (pair == null) {
      return false;
    }

    final Set<String> externalJarsSet = AndroidJpsUtil.getExternalLibraries(paths, module);
    final File resPackage = getPackagedResourcesFile(module, outputDir);

    final File classesDexFile = new File(outputDir.getPath(), AndroidCommonUtils.CLASSES_FILE_NAME);

    final String sdkPath = pair.getFirst().getSdkPath();
    final String outputPath = AndroidJpsUtil.getApkPath(facet, outputDir);
    if (outputPath == null) {
      context.processMessage(
          new CompilerMessage(
              BUILDER_NAME,
              BuildMessage.Kind.ERROR,
              "Cannot compute output path for file " + AndroidJpsUtil.getApkName(module)));
      return false;
    }
    final String customKeyStorePath =
        FileUtil.toSystemDependentName(facet.getCustomDebugKeyStorePath());
    final String[] nativeLibDirs = collectNativeLibsFolders(facet);

    final String resPackagePath =
        release ? resPackage.getPath() + RELEASE_SUFFIX : resPackage.getPath();
    final String outputApkPath = release ? outputPath + UNSIGNED_SUFFIX : outputPath;
    final String classesDexFilePath = classesDexFile.getPath();
    final String[] externalJars = ArrayUtil.toStringArray(externalJarsSet);

    final AndroidFileSetState currentFileSetState =
        buildCurrentApkBuilderState(
            context.getProject(),
            resPackagePath,
            classesDexFilePath,
            nativeLibDirs,
            sourceRoots,
            externalJars,
            release);

    final AndroidApkBuilderConfigState currentApkBuilderConfigState =
        new AndroidApkBuilderConfigState(outputApkPath, customKeyStorePath);

    final AndroidFileSetState savedApkFileSetState = apkFileSetStorage.getState(module.getName());
    final AndroidApkBuilderConfigState savedApkBuilderConfigState =
        apkBuilderConfigStateStorage.getState(module.getName());

    if (context.isMake()
        && currentFileSetState.equalsTo(savedApkFileSetState)
        && currentApkBuilderConfigState.equalsTo(savedApkBuilderConfigState)) {
      return true;
    }
    context.processMessage(
        new ProgressMessage(
            AndroidJpsBundle.message(
                "android.jps.progress.packaging", AndroidJpsUtil.getApkName(module))));

    final Map<AndroidCompilerMessageKind, List<String>> messages =
        AndroidApkBuilder.execute(
            resPackagePath,
            classesDexFilePath,
            sourceRoots,
            externalJars,
            nativeLibDirs,
            outputApkPath,
            release,
            sdkPath,
            customKeyStorePath,
            new MyExcludedSourcesFilter(context.getProject()));

    AndroidJpsUtil.addMessages(context, messages, BUILDER_NAME);
    final boolean success = messages.get(AndroidCompilerMessageKind.ERROR).isEmpty();

    apkFileSetStorage.update(module.getName(), success ? currentFileSetState : null);
    apkBuilderConfigStateStorage.update(
        module.getName(), success ? currentApkBuilderConfigState : null);
    return success;
  }
  private static boolean doResourcePackaging(
      @NotNull CompileContext context,
      @NotNull Collection<Module> modules,
      @NotNull Map<Module, AndroidFileSetState> resourcesStates,
      @NotNull Map<Module, AndroidFileSetState> assetsStates)
      throws IOException {
    boolean success = true;

    final File dataStorageRoot = context.getDataManager().getDataStorageRoot();
    final boolean releaseBuild = AndroidJpsUtil.isReleaseBuild(context);
    AndroidFileSetStorage resourcesStorage = null;
    AndroidFileSetStorage assetsStorage = null;

    try {
      final String resourcesStorageName =
          releaseBuild ? "resources_packaging_release" : "resources_packaging_dev";
      resourcesStorage = new AndroidFileSetStorage(dataStorageRoot, resourcesStorageName);

      final String assetsStorageName =
          releaseBuild ? "assets_packaging_release" : "assets_packaging_dev";
      assetsStorage = new AndroidFileSetStorage(dataStorageRoot, assetsStorageName);

      final Set<Module> modulesToUpdateState = new HashSet<Module>();

      for (Module module : modules) {
        final AndroidFacet facet = AndroidJpsUtil.getFacet(module);
        if (facet == null) {
          continue;
        }

        boolean updateState = true;

        if (!facet.isLibrary()
            && !(context.isMake()
                && checkUpToDate(module, resourcesStates, resourcesStorage)
                && checkUpToDate(module, assetsStates, assetsStorage))) {

          updateState = packageResources(facet, context);

          if (!updateState) {
            success = false;
          }
        }
        if (updateState) {
          modulesToUpdateState.add(module);
        }
      }

      for (Module module : modules) {
        final boolean updateState = modulesToUpdateState.contains(module);
        resourcesStorage.update(module.getName(), updateState ? resourcesStates.get(module) : null);
        assetsStorage.update(module.getName(), updateState ? assetsStates.get(module) : null);
      }
    } finally {
      if (resourcesStorage != null) {
        resourcesStorage.close();
      }

      if (assetsStorage != null) {
        assetsStorage.close();
      }
    }
    return success;
  }