package build.pluto.builder;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.sugarj.common.Log;

import build.pluto.BuildUnit;
import build.pluto.BuildUnit.ModuleVisitor;
import build.pluto.dependency.BuildRequirement;
import build.pluto.dependency.DuplicateBuildUnitPathException;
import build.pluto.dependency.DuplicateFileGenerationException;
import build.pluto.dependency.FileRequirement;
import build.pluto.dependency.IllegalDependencyException;
import build.pluto.dependency.Requirement;
import build.pluto.output.Output;
import build.pluto.util.AbsoluteComparedFile;
import build.pluto.xattr.Xattr;

import com.cedarsoftware.util.DeepEquals;
import com.cedarsoftware.util.Traverser;

public class DynamicAnalysis {
  public final static Xattr XATTR = Xattr.getDefault();
  
  private Map<File, BuildUnit<?>> generatedFiles;
  private Map<Output, BuildUnit<?>> generatedOutput;

  public DynamicAnalysis() {
    this.generatedFiles = new HashMap<>();
    this.generatedOutput = new HashMap<>();
  }
  
  public void reset(BuildUnit<?> unit) throws IOException {
    if (unit != null) {
      for (File f : unit.getGeneratedFiles()) {
        XATTR.removeGenBy(f);
        generatedFiles.remove(f);
      }
    }
  }

  public void check(BuildUnit<?> unit, Integer inputHash) throws IOException {
    checkInput(unit, inputHash);
    checkGeneratedFilesOverlap(unit);
    checkUnitDependency(unit);
    checkGeneratedOutputs(unit);
  }

  /**
   * The input may not have been changed during the build.
   */
  private void checkInput(BuildUnit<?> unit, Integer inputHash) throws AssertionError {
    if (inputHash != null && inputHash != DeepEquals.deepHashCode(unit.getGeneratedBy().input))
      throw new AssertionError("API Violation detected: Builder mutated its input.");
  }

  /**
   * The build unit must have a unique persistent path and may not
   * generated files previously generated by another build unit.
   */
  private void checkGeneratedFilesOverlap(BuildUnit<?> unit) throws IOException {
    BuildUnit<?> other = generatedFiles.put(unit.getPersistentPath(), unit);
    if (other != null && other != unit)
      throw new DuplicateBuildUnitPathException("Build unit " + unit + " has same persistent path as build unit " + other);

    for (FileRequirement freq : unit.getGeneratedFileRequirements()) {
      XATTR.setGenBy(freq.file, unit);
      other = generatedFiles.put(freq.file, unit);
      if (other != null && other != unit)
        throw new DuplicateFileGenerationException("Build unit " + unit + " generates same file as build unit " + other);
    }
  }

  /**
   * When a build unit A requires a file that was generated by a build unit B,
   * then build unit A must already have a build requirement on build unit B.
   */
  private void checkUnitDependency(BuildUnit<?> unit) {
    Set<BuildUnit<?>> requiredUnits = new HashSet<>();
    
    for (Requirement req : unit.getRequirements()) {
      if (req instanceof BuildRequirement<?>)
        requiredUnits.add(((BuildRequirement<?>) req).getUnit());
      else if (req instanceof FileRequirement) {
        File file = ((FileRequirement) req).file;
        if (file.exists()) {
          File dep = null;
          try {
            dep = XATTR.getGenBy(file);
          } catch (IOException e) {
            Log.log.log("WARNING: Could not verify build-unit dependency due to exception \"" + e.getMessage() + "\" while reading metadata: " + file, Log.IMPORT);
          }
          if (dep != null) {

            boolean foundDep = unit.visit(new IsConnectedTo(dep), requiredUnits);
            if (!foundDep)
              throw new IllegalDependencyException(dep, 
                  "Build unit " + unit.getPersistentPath() + " has a hidden dependency on file " + file 
                + " without build-unit dependency on " + dep + ", which generated this file. "
                + "The builder " + unit.getGeneratedBy().createBuilder().description() + " should "
                + "mark a dependency to " + dep + " by `requiring` the corresponding builder.");
          }
        }
      }
    }
  }
  
  /**
   * A builder must declare build requirements on all builders whose
   * outputs it uses (including outputs provided via the build input).
   */
  private void checkGeneratedOutputs(final BuildUnit<?> unit) {
    if (unit.getBuildResult() != null)
      generatedOutput.put(unit.getBuildResult(), unit);
    
    Traverser.traverse(unit.getGeneratedBy().input, new Traverser.Visitor() {
      @Override
      public void process(Object o) {
        if (o instanceof Output) {
          BuildUnit<?> generator = generatedOutput.get(o);
          if (generator != null) {
            File dep = unit.getPersistentPath();
            boolean foundDep = unit.visit(new IsConnectedTo(dep));
            if (!foundDep)
              throw new IllegalDependencyException(dep, 
                  "Build unit " + dep + " has a hidden dependency on the "
                + "in-memory output of build unit " + generator + ". "
                + "The builder " + unit.getGeneratedBy().createBuilder().description() + " should "
                + "mark a dependency to " + dep + " by `requiring` the corresponding builder.");
          }
        }
      }
    });
  }
  
  
  
  
  private static class IsConnectedTo implements ModuleVisitor<Boolean> {
    private final File requiredUnit;
    
    public IsConnectedTo(File requiredUnit) {
      this.requiredUnit = Objects.requireNonNull(requiredUnit);
      Log.log.log("Is Connected to for " + requiredUnit, Log.ALWAYS);
    }
    
    @Override
    public Boolean visit(BuildUnit<?> mod) {

      Log.log.log("Visit " + mod.getPersistentPath(), Log.ALWAYS);
      return AbsoluteComparedFile.equals(requiredUnit, mod.getPersistentPath());
    }

    @Override
    public Boolean combine(Boolean t1, Boolean t2) {
      return t1 || t2;
    }

    @Override
    public Boolean init() {
      return false;
    }

    @Override
    public boolean cancel(Boolean t) {
      return t;
    }
  }
}

@@ -5637,211 +5637,73 @@
     
-  Log.log.log(%22Is Connected to for %22 + requiredUnit, Log.ALWAYS);%0A    %7D%0A    %0A    @Override%0A    public Boolean visit(BuildUnit%3C?%3E mod) %7B%0A%0A      Log.log.log(%22Visit %22 + mod.getPersistentPath(), Log.ALWAYS);
+%7D%0A    %0A    @Override%0A    public Boolean visit(BuildUnit%3C?%3E mod) %7B
 %0A   

Remove temporary log