package fr.monlouyan.bakefile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Parseur de fichier Bakefile.
 * Cette classe est responsable de l'analyse syntaxique du fichier Bakefile
 * pour extraire les règles de build, les dépendances et les commandes associées.
 * 
 * @author Moncef STITI, Yanis HAMOUDI, Louay DARDOURI
 * @version 1.0
 */
public class BakefileParser {
	
	/**
     * Nom du fichier Bakefile à parser (donc Bakefile...).
     */
    private String filename;

	/**
     * Regex pour détecter les targets et leurs dépendances.
     * Format : "nom1 nom2 nom3 : dépendance1 dépendance2"
     * La nouvelle regex gère plusieurs cibles séparées par des espaces
     */
    private static final Pattern TARGET_PATTERN = Pattern.compile("^([^:#]+?)\\s*:\\s*([^#]*?)\\s*(?:#.*)?$");

	/**
     * Regex pour détecter les lignes de commande associées à une target.
     * Format : "    gcc -o program program.c" (ligne indentée)
     */
    private static final Pattern COMMAND_PATTERN = Pattern.compile("^\\t(.+)$");

	/**
     * Regex pour détecter les définitions de variables.
     * Format : "FLAGS = -ansi -pedantic"
     */
    private static final Pattern VARIABLE_PATTERN = Pattern.compile("^(\\w+)\\s*=\\s*([^#]*?)\\s*(?:#.*)?$");

	/**
     * Regex pour détecter les déclarations .PHONY
     * Format : ".PHONY: clean all"
     */
    private static final Pattern PHONY_PATTERN = Pattern.compile("^\\.PHONY:\\s*([^#]*?)\\s*(?:#.*)?$");

	/**
	 * Regex pour détecter les lignes de continuation.
	 * Format : "    gcc -o program program.c \"
	 */
    private static final Pattern CONTINUATION_PATTERN = Pattern.compile("^(.*)\\\\\\s*$");

	/**
	 * Regex pour détecter les références de variables.
	 * Format : "${VAR}" ou "$(VAR)"
	 */
    private static final Pattern VARIABLE_REFERENCE = Pattern.compile("\\$\\{(\\w+)\\}|\\$\\((\\w+)\\)");
    
	/**
     * Première cible trouvée dans le fichier Bakefile.
     */
    private static String firstTarget;

	/**
     * Stocke les variables définies dans le Bakefile.
     */
    private Map<String, String> variables = new HashMap<>();
    
	/**
	 * Constructeur de la classe BakefileParser.
	 * @param filename Nom du fichier Bakefile à parser
	 */
    public BakefileParser(String filename) {
        this.filename = filename;
        firstTarget = null;
    }

	/**
	 * Gérer les lignes de continuation.
	 * @param lines Liste des lignes du fichier Bakefile
	 * @param startIndex Index de la première ligne de continuation
	 * @return La ligne combinée
	 */
    private String handleContinuationLines(List<String> lines, int startIndex) {
        StringBuilder combinedLine = new StringBuilder();
        int i = startIndex;
        
        while (i < lines.size()) {
            String line = lines.get(i);
            Matcher contMatcher = CONTINUATION_PATTERN.matcher(line);
            
            if (contMatcher.matches()) {
                // Ajouter la ligne sans le backslash
                combinedLine.append(contMatcher.group(1).trim()).append(" ");
                i++;
            } else {
                // Ajouter la dernière ligne et sortir
                combinedLine.append(line.trim());
                break;
            }
        }
        
        return combinedLine.toString();
    }

	/**
	 * Remplacer les variables dans une chaîne.
	 * @param input Chaîne à traiter
	 * @return Chaîne avec les variables remplacées
	 */
    private String replaceVariables(String input) {
        if (input == null) return null;
        
        String result = input;
        Set<String> processedVars = new HashSet<>();
        boolean changed;
        
        do {
            changed = false;
            Matcher matcher = VARIABLE_REFERENCE.matcher(result);
            StringBuffer sb = new StringBuffer();
            
            while (matcher.find()) {
                String varName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
                if (!processedVars.contains(varName) && variables.containsKey(varName)) {
                    String replacement = variables.get(varName);
                    matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
                    changed = true;
                    processedVars.add(varName);
                }
            }
            matcher.appendTail(sb);
            result = sb.toString();
            
            // Si aucun changement n'a été fait dans ce passage, arrêter
            if (!changed) {
                break;
            }
            
            // Réinitialiser processedVars pour le prochain passage si nécessaire
            processedVars.clear();
            
        } while (changed);
        
        return result.trim();
    }

	/**
	 * Remplacer les variables dans une liste de chaînes.
	 * @param items Liste de chaînes à traiter
	 * @return Liste de chaînes avec les variables remplacées
	 */
    private List<String> replaceVariablesInList(List<String> items) {
        return items.stream()
                   .map(this::replaceVariables)
                   .collect(Collectors.toList());
    }

	/**
	 * Découper les dépendances en une liste de chaînes.
	 * @param depStr Chaîne de dépendances
	 * @return Liste de dépendances
	 */
    private List<String> splitDependencies(String depStr) {
        if (depStr == null || depStr.trim().isEmpty()) {
            return new ArrayList<>();
        }
        
        String resolvedStr = replaceVariables(depStr.trim());
        return Arrays.stream(resolvedStr.split("\\s+"))
                    .map(String::trim)
                    .filter(s -> !s.isEmpty())
                    .collect(Collectors.toList());
    }

	/**
	 * Découper les cibles en une liste de chaînes.
	 * @param targetStr Chaîne de cibles
	 * @return Liste de cibles
	 */
    private List<String> splitTargets(String targetStr) {
        if (targetStr == null || targetStr.trim().isEmpty()) {
            return new ArrayList<>();
        }
        
        String resolvedStr = replaceVariables(targetStr.trim());
        return Arrays.stream(resolvedStr.split("\\s+"))
                    .map(String::trim)
                    .filter(s -> !s.isEmpty())
                    .collect(Collectors.toList());
    }

	/**
	 * Analyser le fichier Bakefile pour extraire les règles de build.
	 * @return Liste des règles extraites
	 */
    public List<Rule> parse() {
        List<Rule> rules = new ArrayList<>();
        Set<String> phonyTargets = new HashSet<>();
    
        if (!Files.exists(Paths.get(filename))) {
            System.out.println("*** No targets specified and no makefile found.  Stop.");
            System.exit(2);
        }       
    
        try {
            List<String> lines = Files.readAllLines(Paths.get(filename));
            List<String> currentTargets = null;
            List<String> dependencies = new ArrayList<>();
            List<String> commands = new ArrayList<>();
            
            for (int i = 0; i < lines.size(); i++) {
                String line = lines.get(i);
                
                // Vérifier si la ligne a un caractère de continuation
                Matcher contMatcher = CONTINUATION_PATTERN.matcher(line);
                if (contMatcher.matches()) {
                    // Récupérer toute la définition multi-ligne
                    line = handleContinuationLines(lines, i);
                    // Ajuster i pour sauter les lignes traitées
                    while (i + 1 < lines.size() && 
                           CONTINUATION_PATTERN.matcher(lines.get(i)).matches()) {
                        i++;
                    }
                }

                if (line.trim().isEmpty()) {
                    continue;
                }

                if (line.matches("^ +.*$")) {
                    System.err.println(filename + ":" + (i+1) + ": *** missing separator.  Stop.");
                    System.exit(2);
                }
                
                Matcher varMatcher = VARIABLE_PATTERN.matcher(line);
                Matcher targetMatcher = TARGET_PATTERN.matcher(line);
                Matcher commandMatcher = COMMAND_PATTERN.matcher(line);
                Matcher phonyMatcher = PHONY_PATTERN.matcher(line);
                
                if (phonyMatcher.matches()) {
                    String[] phonies = phonyMatcher.group(1).trim().split("\\s+");
                    Collections.addAll(phonyTargets, phonies);
                    continue;
                }
                
                if (varMatcher.matches()) {
                    String varName = varMatcher.group(1);
                    String varValue = varMatcher.group(2).trim();
                    // Évaluer les variables référencées dans la valeur
                    varValue = replaceVariables(varValue);
                    variables.put(varName, varValue);
                } else if (targetMatcher.matches()) {
                    if (currentTargets != null) {
                        // Créer une règle pour chaque cible avec les mêmes dépendances et commandes
                        for (String target : currentTargets) {
                            String resolvedTarget = replaceVariables(target.trim());
                            rules.add(new Rule(
                                resolvedTarget,
                                splitDependencies(dependencies.stream()
                                    .collect(Collectors.joining(" "))),
                                replaceVariablesInList(commands),
                                phonyTargets.contains(resolvedTarget)
                            ));
                            
                            if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) {
                                firstTarget = resolvedTarget;
                            }
                        }
                    }
    
                    String targetStr = targetMatcher.group(1);
                    currentTargets = splitTargets(targetStr);
                    String depStr = targetMatcher.group(2);
                    dependencies = splitDependencies(depStr);
                    commands = new ArrayList<>();
                } else if (commandMatcher.matches()) {
                    commands.add(commandMatcher.group(1));
                }
            }
    
            if (currentTargets != null) {
                // Créer une règle pour chaque cible avec les mêmes dépendances et commandes
                for (String target : currentTargets) {
                    String resolvedTarget = replaceVariables(target.trim());
                    rules.add(new Rule(
                        resolvedTarget,
                        replaceVariablesInList(dependencies),
                        replaceVariablesInList(commands),
                        phonyTargets.contains(resolvedTarget)
                    ));
                    
                    if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) {
                        firstTarget = resolvedTarget;
                    }
                }
            }
    
        } catch (IOException e) {
            e.printStackTrace();
        }
        return rules;
    }

	/**
	 * Récupérer la première cible
	 * @return String la première cible
	 */
    public static String getFirstTarget() { 
        return firstTarget; 
    }
}