package dk.camelot64.kickc.model;

import dk.camelot64.kickc.CompileLog;
import dk.camelot64.kickc.asm.AsmProgram;
import dk.camelot64.kickc.model.statements.Statement;
import dk.camelot64.kickc.model.statements.StatementInfos;
import dk.camelot64.kickc.model.symbols.ProgramScope;
import dk.camelot64.kickc.model.values.LabelRef;
import dk.camelot64.kickc.model.values.VariableRef;
import dk.camelot64.kickc.passes.calcs.*;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/** A KickC Intermediate Compiler Language (ICL) Program */
public class Program {

   /** The log containing information about the compilation process. */
   private CompileLog log;

   /** The name of the file being compiled. PASS 0-5 (STATIC) */
   private String fileName;
   /** Paths used for importing files. PASS 0 (STATIC) */
   private List<String> importPaths;
   /** Imported files. PASS 0 (STATIC) */
   private List<String> imported;
   /** Path to any custom link script file used for linking (STATIC) */
   private Path linkScriptFilePath;
   /** Body to any custom link script file used for linking (STATIC) */
   private String linkScriptBody;

   /** The target platform that the program is being build for. PASS 0-5 (STATIC) */
   private TargetPlatform targetPlatform = TargetPlatform.DEFAULT;
   /** Absolute start address of the code. Null to start ad 0x080d. PASS 0-5 (STATIC) */
   private Number programPc;
   /** Reserved ZP addresses that the compiler cannot use. PASS 0-5 (STATIC) */
   private List<Number> reservedZps;
   /** Resource files that should be copied to the output folder to be compiled with the generated ASM. PASS 0-5 (STATIC) */
   private List<Path> asmResourceFiles;
   /** Comments for the (main) file. PASS 0-4 (STATIC) */
   private List<Comment> fileComments;

   /** The initial statement sequence generated by the parser. PASS 1 (STATIC) */
   private StatementSequence statementSequence;
   /** Constants identified during pass 1. PASS 1 (STATIC) */
   private Collection<VariableRef> earlyIdentifiedConstants;
   /** Variables modified inside procedures. PASS 1 (STATIC) */
   private ProcedureModifiedVars procedureModifiedVars;
   /** Struct values unwound to individual variables. PASS 1 (STATIC) */
   private StructUnwinding structUnwinding;

   /** The main scope. PASS 0-5 (DYNAMIC) */
   private ProgramScope scope;
   /** The control flow graph. PASS 1-5 (DYNAMIC) */
   private ControlFlowGraph graph;
   /** Registers potentially usable as allocation for each live range equivalence class. PASS 4 (DYNAMIC) */
   private RegisterPotentials registerPotentials;
   /** Live range equivalence classes containing variables that do not have overlapping live ranges. PASS 3-5 (DYNAMIC) */
   private LiveRangeEquivalenceClassSet liveRangeEquivalenceClassSet;
   /** The 6502 ASM program. PASS 4-5 (DYNAMIC) */
   private AsmProgram asm;

   /** A saved program snapshot that can be rolled back. Used to store the (DYNAMIC) state of the program while trying out a potential optimization. PASS 2 (DYNAMIC) */
   private ProgramSnapshot snapshot;

   /** Cached information about calls. PASS 1-4 (CACHED ON-DEMAND) */
   private CallGraph callGraph;
   /** Cached information about the variables referenced by blocks/statements. PASS 1-4 (CACHED ON-DEMAND) */
   private VariableReferenceInfos variableReferenceInfos;
   /** Information about dominators of all blocks. PASS 2U-4 (CACHED ON-DEMAND) */
   private DominatorsGraph dominators;
   /** Cached information about symbols. Contains a symbol table cache for fast access. PASS 3-4 (CACHED ON-DEMAND) */
   private SymbolInfos symbolInfos;
   /** Cached phi transitions into each block. PASS 4 (CACHED ON-DEMAND) */
   private Map<LabelRef, PhiTransitions> phiTransitions;
   /** The live ranges of all variables. PASS 3-4 (CACHED ON-DEMAND) */
   private LiveRangeVariables liveRangeVariables;
   /** The effective live ranges of all variables. PASS 3-4 (CACHED ON-DEMAND) */
   private LiveRangeVariablesEffective liveRangeVariablesEffective;
   /** Separation of live range equivalence classes into scopes - used for register uplift. PASS 4 (CACHED ON-DEMAND) */
   private RegisterUpliftProgram registerUpliftProgram;
   /** Cached information about which block is each statement a part of. PASS 2U-5 (CACHED ON-DEMAND) */
   private StatementInfos statementInfos;
   /** Information about loops. PASS 2U-5 (CACHED ON-DEMAND) */
   private NaturalLoopSet loopSet;
   /** The register weight of all variables describing how much the variable would theoretically gain from being in a register. PASS 3-5 (CACHED ON-DEMAND) */
   private VariableRegisterWeights variableRegisterWeights;

   public Program() {
      this.scope = new ProgramScope();
      this.log = new CompileLog();
      this.importPaths = new ArrayList<>();
      this.imported = new ArrayList<>();
      this.asmResourceFiles = new ArrayList<>();
      this.reservedZps = new ArrayList<>();
   }

   /**
    * Clears all data that is only used in PASS 1
    */
   public void endPass1() {
      this.importPaths = null;
      this.imported = null;
      this.statementSequence = null;
      this.earlyIdentifiedConstants = null;
      this.procedureModifiedVars = null;
      this.structUnwinding = null;
   }

   /**
    * Clears all data that is only used in PASS 2-4
    */
   public void endPass4() {
      this.snapshot = null;
      this.fileComments = null;
      this.callGraph = null;
      this.variableReferenceInfos = null;
      this.dominators = null;
      this.symbolInfos = null;
      this.phiTransitions = null;
      this.liveRangeVariables = null;
      this.liveRangeVariablesEffective = null;
      this.registerPotentials = null;
      this.registerUpliftProgram = null;
   }

   /** Save a snapshot of the dynamic parts of the program. */
   public void snapshotCreate() {
      if(this.snapshot != null)
         throw new InternalError("Snapshot already saved!");
      if(this.liveRangeEquivalenceClassSet != null)
         throw new InternalError("Compiler Program Snapshot does not support liveRangeEquivalenceClassSet!");
      this.snapshot = new ProgramSnapshot(scope, graph);
   }

   /** Restore the snapshot of the dynamic parts of the program. Clear all cached data and the snapshot. */
   public void snapshotRestore() {
      this.scope = snapshot.getScope();
      this.graph = snapshot.getGraph();
      this.snapshot = null;
      this.callGraph = null;
      this.variableReferenceInfos = null;
      this.dominators = null;
      this.loopSet = null;
      this.statementInfos = null;
      this.symbolInfos = null;
      this.phiTransitions = null;
      this.liveRangeVariables = null;
      this.liveRangeVariablesEffective = null;
      this.variableRegisterWeights = null;
      this.registerPotentials = null;
      this.registerUpliftProgram = null;
      this.asm = null;
   }

   public TargetPlatform getTargetPlatform() {
      return targetPlatform;
   }

   public void setTargetPlatform(TargetPlatform targetPlatform) {
      this.targetPlatform = targetPlatform;
   }

   public StructUnwinding getStructUnwinding() {
      return structUnwinding;
   }

   public void setStructUnwinding(StructUnwinding structUnwinding) {
      this.structUnwinding = structUnwinding;
   }

   public List<Comment> getFileComments() {
      return fileComments;
   }

   public void setFileComments(List<Comment> fileComments) {
      this.fileComments = fileComments;
   }

   public List<String> getImportPaths() {
      return importPaths;
   }

   public List<String> getImported() {
      return imported;
   }

   public List<Path> getAsmResourceFiles() {
      return asmResourceFiles;
   }

   public void addAsmResourceFile(Path asmResourceFile) {
      asmResourceFiles.add(asmResourceFile);
   }

   public StatementSequence getStatementSequence() {
      return statementSequence;
   }

   public void setStatementSequence(StatementSequence statementSequence) {
      this.statementSequence = statementSequence;
   }

   public ProgramScope getScope() {
      return scope;
   }

   public void setScope(ProgramScope scope) {
      this.scope = scope;
   }

   public ControlFlowGraph getGraph() {
      return graph;
   }

   public void setGraph(ControlFlowGraph graph) {
      this.graph = graph;
   }

   public ProcedureModifiedVars getProcedureModifiedVars() {
      return procedureModifiedVars;
   }

   public void setProcedureModifiedVars(ProcedureModifiedVars procedureModifiedVars) {
      this.procedureModifiedVars = procedureModifiedVars;
   }

   public AsmProgram getAsm() {
      return asm;
   }

   public void setAsm(AsmProgram asm) {
      this.asm = asm;
   }

   /**
    * Get the call-graph for the program. Calculates the call-graph if it has not already been calculated.
    * @return The call-graph
    */
   public CallGraph getCallGraph() {
      if(callGraph == null)
         this.callGraph = new PassNCalcCallGraph(this).calculate();
      return callGraph;
   }

   /**
    * Clears the call-graph ensuring it will be re-calculated if used.
    */
   public void clearCallGraph() {
      this.callGraph = null;
   }

   public VariableReferenceInfos getVariableReferenceInfos() {
      if(variableReferenceInfos==null)
         this.variableReferenceInfos = new PassNCalcVariableReferenceInfos(this).calculate();
      return variableReferenceInfos;
   }

   public void clearVariableReferenceInfos() {
      this.variableReferenceInfos = null;
   }

   public DominatorsGraph getDominators() {
      if(dominators==null)
         this.dominators = new PassNCalcDominators(this).calculate();
      return dominators;
   }

   public void clearDominators() {
      this.dominators = null;
   }

   public Map<LabelRef, PhiTransitions> getPhiTransitions() {
      if(phiTransitions==null)
         this.phiTransitions = new PassNCalcPhiTransitions(this).calculate();
      return phiTransitions;
   }

   public void clearPhiTransitions() {
      this.phiTransitions = null;
   }

   public LiveRangeVariables getLiveRangeVariables() {
      if(liveRangeVariables==null)
         this.liveRangeVariables = new PassNCalcLiveRangeVariables(this).calculate();
      return liveRangeVariables;
   }

   public void setLiveRangeVariables(LiveRangeVariables liveRangeVariables) {
      this.liveRangeVariables = liveRangeVariables;
   }

   public void clearLiveRangeVariables() {
      this.liveRangeVariables = null;
   }

   public StatementInfos getStatementInfos() {
      if(statementInfos==null)
         this.statementInfos = new PassNCalcStatementInfos(this).calculate();
      return statementInfos;
   }

   public void clearStatementInfos() {
      this.statementInfos = null;
   }

   /**
    * Clear index numbers for all statements in the control flow graph.
    */
   public void clearStatementIndices() {
      for(ControlFlowBlock block : getGraph().getAllBlocks()) {
         for(Statement statement : block.getStatements()) {
            statement.setIndex(null);
         }
      }
   }

   public SymbolInfos getSymbolInfos() {
      if(symbolInfos==null)
         this.symbolInfos = new PassNCalcSymbolInfos(this).calculate();
      return symbolInfos;
   }

   public LiveRangeVariablesEffective getLiveRangeVariablesEffective() {
      if(liveRangeVariablesEffective==null)
         this.liveRangeVariablesEffective = new PassNCalcLiveRangesEffective(this).calculate();
      return liveRangeVariablesEffective;
   }

   public void clearLiveRangeVariablesEffective() {
      this.liveRangeVariablesEffective = null;
   }

   public RegisterUpliftProgram getRegisterUpliftProgram() {
      if(registerUpliftProgram==null)
         this.registerUpliftProgram = new PassNCalcRegisterUpliftProgram(this).calculate();
      return registerUpliftProgram;
   }

   public NaturalLoopSet getLoopSet() {
      if(loopSet==null)
         this.loopSet = new PassNCalcLoopSet(this).calculate();
      return loopSet;
   }

   public void clearLoopSet() {
      this.loopSet = null;
   }

   public VariableRegisterWeights getVariableRegisterWeights() {
      if(variableRegisterWeights==null)
         this.variableRegisterWeights = new PassNCalcVariableRegisterWeight(this).calculate();
      return variableRegisterWeights;
   }

   public VariableRegisterWeights getOrNullVariableRegisterWeights() {
      return variableRegisterWeights;
   }

   public LiveRangeEquivalenceClassSet getLiveRangeEquivalenceClassSet() {
      return liveRangeEquivalenceClassSet;
   }

   public void setLiveRangeEquivalenceClassSet(LiveRangeEquivalenceClassSet liveRangeEquivalenceClassSet) {
      this.liveRangeEquivalenceClassSet = liveRangeEquivalenceClassSet;
   }

   public RegisterPotentials getRegisterPotentials() {
      return registerPotentials;
   }

   public void setRegisterPotentials(RegisterPotentials registerPotentials) {
      this.registerPotentials = registerPotentials;
   }

   public Collection<VariableRef> getEarlyIdentifiedConstants() {
      return earlyIdentifiedConstants;
   }

   public void setEarlyIdentifiedConstants(Collection<VariableRef> earlyIdentifiedConstants) {
      this.earlyIdentifiedConstants = earlyIdentifiedConstants;
   }


   public void setFileName(String fileName) {
      this.fileName = fileName;
   }

   public String getFileName() {
      return fileName;
   }

   /**
    * Adds a bunch of reserved zero-page addresses that the compiler is not allowed to use.
    *
    * @param reservedZp addresses to reserve
    */
   public void addReservedZps(List<Number> reservedZp) {
      for(Number zp : reservedZp) {
         if(!this.reservedZps.contains(zp)) {
            this.reservedZps.add(zp);
         }
      }
   }

   public List<Number> getReservedZps() {
      return reservedZps;
   }

   /**
    * Set the absolute position of the program code
    *
    * @param programPc The address
    */
   public void setProgramPc(Number programPc) {
      this.programPc = programPc;
   }

   public Number getProgramPc() {
      return programPc;
   }

   public CompileLog getLog() {
      return log;
   }

   public void setLog(CompileLog log) {
      this.log = log;
   }

   public void setLinkScript(Path linkScriptFilePath, String linkScriptBody) {
      this.linkScriptFilePath = linkScriptFilePath;
      this.linkScriptBody = linkScriptBody;
   }

   public Path getLinkScriptFilePath() {
      return linkScriptFilePath;
   }

   public String getLinkScriptBody() {
      return linkScriptBody;
   }
}
