(********************************************************************)
(*                                                                  *)
(*  make.s7i      Make library to manage the compilation process    *)
(*  Copyright (C) 2010 - 2014  Thomas Mertes                        *)
(*                                                                  *)
(*  This file is part of the Seed7 Runtime Library.                 *)
(*                                                                  *)
(*  The Seed7 Runtime Library is free software; you can             *)
(*  redistribute it and/or modify it under the terms of the GNU     *)
(*  Lesser General Public License as published by the Free Software *)
(*  Foundation; either version 2.1 of the License, or (at your      *)
(*  option) any later version.                                      *)
(*                                                                  *)
(*  The Seed7 Runtime Library is distributed in the hope that it    *)
(*  will be useful, but WITHOUT ANY WARRANTY; without even the      *)
(*  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR *)
(*  PURPOSE.  See the GNU Lesser General Public License for more    *)
(*  details.                                                        *)
(*                                                                  *)
(*  You should have received a copy of the GNU Lesser General       *)
(*  Public License along with this program; if not, write to the    *)
(*  Free Software Foundation, Inc., 51 Franklin Street,             *)
(*  Fifth Floor, Boston, MA  02110-1301, USA.                       *)
(*                                                                  *)
(********************************************************************)


include "makedata.s7i";
include "stdio.s7i";
include "osfiles.s7i";
include "time.s7i";
include "cli_cmds.s7i";


const type: makeFlag is new enum
    ignoreErrors. dontExecute, silentMode
  end enum;

const type: makeFlags is set of makeFlag;


const proc: make (in string: makefile, in array string: targets, in makeFlags: flags) is forward;


const proc: doMake (inout string: parameters) is func
  local
    var string: aParam is "";
    var string: makefile is "";
    var array string: targets is 0 times "";
    var makeFlags: flags is makeFlags.value;
    var string: savedCurrentDir is "";
  begin
    # writeln("doMake(" <& literal(parameters) <& ")");
    savedCurrentDir := getcwd;
    skipWhiteSpace(parameters);
    while parameters <> "" and parameters[1] in parameter_char do
      aParam := getCommandParameter(parameters);
      if length(aParam) >= 2 and aParam[1] = '-' then
        if aParam = "-C" then
          skipWhiteSpace(parameters);
          aParam := convDosPath(getCommandParameter(parameters));
          chdir(aParam);
        elsif aParam = "-f" then
          skipWhiteSpace(parameters);
          makefile := convDosPath(getCommandParameter(parameters));
        elsif aParam = "-i" then
          incl(flags, ignoreErrors);
        elsif aParam = "-n" then
          incl(flags, dontExecute);
        elsif aParam = "-s" then
          incl(flags, silentMode);
        end if;
      else
        targets &:= aParam;
      end if;
      skipWhiteSpace(parameters);
    end while;
    if makefile = "" then
      if fileType("makefile") = FILE_REGULAR then
        makefile := "makefile";
      elsif fileType("Makefile") = FILE_REGULAR then
        makefile := "Makefile";
      end if;
    end if;
    make(makefile, targets, flags);
    chdir(savedCurrentDir);
  end func;


const func string: expandInternalMacro (in char: macro, in ruleType: rule,
    in array string: strictDependencies) is func
  result
    var string: expandedMacro is "";
  begin
    case macro of
      when {'@'}:
          expandedMacro := rule.target;
      when {'<'}:
          if length(rule.dependencies) <> 0 then
            expandedMacro := rule.dependencies[1];
          end if;
      when {'?'}:
          if length(strictDependencies) <> 0 then
            expandedMacro := join(strictDependencies, " ");
          end if;
      when {'^'}:
          if length(strictDependencies) <> 0 then
            expandedMacro := join(strictDependencies, " ");
          end if;
      when {'+'}:
          if length(rule.dependencies) <> 0 then
            expandedMacro := join(rule.dependencies, " ");
          end if;
    end case;
  end func;


const func string: applyInternalMacros (in ruleType: rule, in string: stri) is func
  result
    var string: macrosApplied is "";
  local
    const set of char: iternalMacroDesignator is {'@', '<', '?', '^', '+'};
    var integer: dollarPos is 0;
    var integer: pos is 0;
    var string: dependency is "";
    var set of string: dependencySet is (set of string).value;
    var array string: strictDependencies is 0 times "";
    var char: macro is ' ';
    var string: suffix is "";
    var string: replacement is "";
  begin
    for dependency range rule.dependencies do
      if dependency not in dependencySet then
        strictDependencies &:= dependency;
        incl(dependencySet, dependency);
      end if;
    end for;
    macrosApplied := stri;
    dollarPos := pos(macrosApplied, '$');
    while dollarPos <> 0 do
      if dollarPos < length(macrosApplied) then
        if macrosApplied[succ(dollarPos)] in iternalMacroDesignator then
          macrosApplied := macrosApplied[.. pred(dollarPos)] &
              expandInternalMacro(macrosApplied[succ(dollarPos)], rule, strictDependencies) &
              macrosApplied[dollarPos + 2 ..];
        elsif macrosApplied[succ(dollarPos)] = '(' then
          pos := dollarPos + 2;
          if pos <= length(macrosApplied) and macrosApplied[pos] in iternalMacroDesignator then
            macro := macrosApplied[pos];
            incr(pos);
            if pos <= length(macrosApplied) then
              if macrosApplied[pos] = ')' then
                macrosApplied := macrosApplied[.. pred(dollarPos)] &
                    expandInternalMacro(macro, rule, strictDependencies) &
                    macrosApplied[succ(pos) ..];
              elsif macrosApplied[pos] = ':' then
                incr(pos);
                suffix := "";
                while pos <= length(macrosApplied) and macrosApplied[pos] <> '=' do
                  suffix &:= macrosApplied[pos];
                  incr(pos);
                end while;
                if pos <= length(macrosApplied) then
                  incr(pos);
                  replacement := "";
                  while pos <= length(macrosApplied) and macrosApplied[pos] <> ')' do
                    replacement &:= macrosApplied[pos];
                    incr(pos);
                  end while;
                  if pos <= length(macrosApplied) then
                    macrosApplied := macrosApplied[.. pred(dollarPos)] &
                        replaceSuffixes(expandInternalMacro(macro, rule, strictDependencies), suffix, replacement) &
                        macrosApplied[succ(pos) ..];
                  end if;
                end if;
              end if;
            end if;
          end if;
        elsif macrosApplied[succ(dollarPos)] = '$' then
          macrosApplied := macrosApplied[.. pred(dollarPos)] & macrosApplied[succ(dollarPos) ..];
        end if;
      end if;
      dollarPos := pos(macrosApplied, '$', succ(dollarPos));
    end while;
  end func;


const proc: processCommands (in makeDataType: makeData, inout ruleType: rule) is func
  local
    var string: command is "";
    var integer: commandStatus is 0;
    var boolean: printCommand is TRUE;
    var boolean: ignoreError is FALSE;
  begin
    for command range rule.commands do
      command := applyInternalMacros(rule, command);
      printCommand := TRUE;
      ignoreError := FALSE;
      if startsWith(command, "@-") or startsWith(command, "-@") then
        printCommand := FALSE;
        ignoreError := TRUE;
        command := command[3 ..];
      elsif startsWith(command, "@") then
        printCommand := FALSE;
        command := command[2 ..];
      elsif startsWith(command, "-") then
        ignoreError := TRUE;
        command := command[2 ..];
      end if;
      command := applyMacros(makeData.macros, command, FALSE);
      if command <> "" then
        if printCommand and not makeData.inSilentMode or
            not printCommand and not makeData.executeCommands then
          writeln(command);
          flush(STD_OUT);
        end if;
        if makeData.executeCommands then
          commandStatus := processCommand(command);
          if commandStatus <> 0 and not (ignoreError or makeData.doIgnoreErrors) then
            writeln(" *** [" <& rule.target <& "] Error " <& commandStatus);
            raise FILE_ERROR;
          end if;
        end if;
      end if;
    end for;
  end func;


const func string: pattern_match (in var string: main_stri, in string: pattern) is func
  result
    var string: stem is "";
  local
    var integer: percentPos is 0;
    var integer: main_index is 2;
    var string: pattern_tail is "";
  begin
    if startsWith(main_stri, "./") then
      main_stri := main_stri[3 ..];
    end if;
    percentPos := pos(pattern, '%');
    if percentPos = rpos(pattern, '%') then
      if startsWith(main_stri, pattern[.. pred(percentPos)]) and
          endsWith(main_stri, pattern[succ(percentPos) ..]) then
        stem := main_stri[percentPos .. length(main_stri) - length(pattern) + percentPos];
      end if;
    end if;
  end func;


const proc: processRule (inout makeDataType: makeData, in string: targetName) is forward;


const proc: processRule (inout makeDataType: makeData, inout ruleType: rule) is func
  local
    var string: dependency is "";
    var string: dependencyFile is "";
    var string: ruleFile is "";
    var time: targetTime is time.value;
    var boolean: commandsNecessary is FALSE;
  begin
    (* write(rule.target <& ":");
      for dependency range rule.dependencies do
        write(" " <& dependency);
      end for;
      writeln; *)
    if not rule.ruleProcessed then
      for dependency range rule.dependencies do
        processRule(makeData, dependency);
      end for;
      ruleFile := convDosPath(rule.target);
      if fileType(ruleFile) = FILE_ABSENT then
        processCommands(makeData, rule);
      else
        targetTime := getMTime(ruleFile);
        for dependency range rule.dependencies do
          dependencyFile := convDosPath(applyMacros(makeData.macros, dependency, FALSE));
          if dependencyFile <> "" then
            if fileType(dependencyFile) = FILE_ABSENT then
              if makeData.executeCommands then
                writeln(" *** File " <& dependencyFile <& " missing");
              else
                commandsNecessary := TRUE;
              end if;
            elsif getMTime(dependencyFile) > targetTime then
              commandsNecessary := TRUE;
            end if;
          end if;
        end for;
        if commandsNecessary then
          processCommands(makeData, rule);
        end if;
      end if;
      rule.ruleProcessed := TRUE;
    end if;
  end func;


const proc: processPatternRule (inout makeDataType: makeData, in string: targetName,
    inout ruleType: dependencyRule) is func
  local
    var string: ruleName is "";
    var integer: ruleIndex is 0;
    var string: stem is "";
    var boolean: found is FALSE;
    var string: ruleNameFound is "";
    var integer: ruleIndexFound is 1;
    var integer: numberOfRulesFound is 1;
    var string: stemFound is "";
    var ruleType: patternRule is ruleType.value;
    var ruleType: actualRule is ruleType.value;
    var string: dependency is "";
    var string: actualDependency is "";
  begin
    for key ruleName range makeData.patternRules do
      # writeln("try " <& ruleName);
      stem := pattern_match(targetName, ruleName);
      if stem <> "" then
        # writeln("Pattern rule: " <& ruleName <& "[1] stem: " <& stem);
        if not found or length(stem) < length(stemFound) then
          stemFound := stem;
          ruleNameFound := ruleName;
        end if;
        found := TRUE;
      end if;
    end for;
    if found then
      # writeln("found: " <& ruleNameFound <& " " <& length(makeData.patternRules[ruleNameFound]));
      if length(makeData.patternRules[ruleNameFound]) <> 1 then
        # There are several pattern rules that match.
        numberOfRulesFound := 0;
        for key ruleIndex range makeData.patternRules[ruleNameFound] do
          for dependency range makeData.patternRules[ruleNameFound][ruleIndex].dependencies do
            if pos(dependency, "%") <> 0 then
              actualDependency := replace(dependency, "%", stemFound);
              if fileType(convDosPath(actualDependency)) <> FILE_ABSENT or
                  actualDependency in makeData.rules then
                # Take a rule if the file exists or a rule for the file exists.
                ruleIndexFound := ruleIndex;
                incr(numberOfRulesFound);
              end if;
            end if;
          end for;
        end for;
      end if;
      if numberOfRulesFound = 1 then
        patternRule := makeData.patternRules[ruleNameFound][ruleIndexFound];
        actualRule.target := targetName;
        if stemFound = targetName then
          # Avoid rule recursion.
          for dependency range patternRule.dependencies do
            if pos(dependency, "%") <> 0 then
              actualDependency := replace(dependency, "%", stemFound);
              if fileType(convDosPath(actualDependency)) <> FILE_ABSENT or
                  actualDependency in makeData.rules then
                actualRule.dependencies &:= actualDependency;
              end if;
            else
              actualRule.dependencies &:= dependency;
            end if;
          end for;
        else
          for dependency range patternRule.dependencies do
            actualRule.dependencies &:= replace(dependency, "%", stemFound);
          end for;
        end if;
        for dependency range dependencyRule.dependencies do
          actualRule.dependencies &:= dependency;
        end for;
        actualRule.commands := patternRule.commands;
        processRule(makeData, actualRule);
      else
        processRule(makeData, dependencyRule);
      end if;
    else
      processRule(makeData, dependencyRule);
    end if;
  end func;


const proc: processRule (inout makeDataType: makeData, in string: targetName) is func
  local
    var string: ruleName is "";
    var ruleType: emptyRule is ruleType.value;
  begin
    ruleName := applyMacros(makeData.macros, targetName, FALSE);
    if ruleName <> "" then
      # writeln("process rule: " <& ruleName);
      if ruleName in makeData.rules then
        if length(makeData.rules[ruleName].commands) <> 0 then
          processRule(makeData, makeData.rules[ruleName]);
        else
          processPatternRule(makeData, ruleName, makeData.rules[ruleName]);
        end if;
      else
        processPatternRule(makeData, ruleName, emptyRule);
      end if;
    end if;
  end func;


(**
 *  Use rules and commands from a makefile to create the specified ''targets''.
 *  @param makefile Path of the makefile which contains rules and commands
 *                  in the usual makefile syntax.
 *  @param targets The targets that should be made.
 *  @param flags Options which affect command execution
 *  @param macros Predefined make macros.
 *  @exception FILE_ERROR A command failed and the flag ignoreErrors
 *                        has not been set (ignoreErrors not in flags holds).
 *)
const proc: make (in string: makefile, in array string: targets, in makeFlags: flags,
    in stringHash: macros) is func
  local
    var ruleType: rule is ruleType.value;
    var makeDataType: makeData is makeDataType.value;
    var string: target is "";
  begin
    # writeln("make -f " <& makefile <& " " <& join(targets, " "));
    makeData.executeCommands := dontExecute not in flags;
    makeData.inSilentMode := silentMode in flags;
    makeData.doIgnoreErrors := ignoreErrors in flags;
    makeData.macros := macros;
    if "CPPFLAGS" not in makeData.macros then
      makeData.macros @:= ["CPPFLAGS"] "";
    end if;
    if "CXXFLAGS" not in makeData.macros then
      makeData.macros @:= ["CXXFLAGS"] "";
    end if;
    if "CFLAGS" not in makeData.macros then
      makeData.macros @:= ["CFLAGS"] "";
    end if;
    if "CC" not in makeData.macros then
      makeData.macros @:= ["CC"] "cc";
    end if;
    if "CXX" not in makeData.macros then
      makeData.macros @:= ["CXX"] "c++";
    end if;
    makeData.macros @:= ["MAKE"] "make7";
    if length(targets) <> 0 then
      makeData.macros @:= ["MAKECMDGOALS"] join(targets, " ");
    end if;
    readMakefile(makeData, makefile);
    if not patternRulePresent(makeData, "%.o", "%.c") then
      if ".c.o" in makeData.rules then
        rule.target := "%.o";
        rule.dependencies := [] ("%.c");
        rule.commands := makeData.rules[".c.o"].commands;
        addPatternRule(makeData, rule);
        excl(makeData.rules, ".c.o");
      else
        rule.target := "%.o";
        rule.dependencies := [] ("%.c");
        rule.commands := [] (applyMacros(makeData.macros, "$(CC) -c $(CPPFLAGS) $(CFLAGS) $< -o $@", TRUE));
        addPatternRule(makeData, rule);
      end if;
    end if;
    if not patternRulePresent(makeData, "%.obj", "%.c") then
      if ".c.obj" in makeData.rules then
        rule.target := "%.obj";
        rule.dependencies := [] ("%.c");
        rule.commands := makeData.rules[".c.obj"].commands;
        addPatternRule(makeData, rule);
        excl(makeData.rules, ".c.obj");
      else
        rule.target := "%.obj";
        rule.dependencies := [] ("%.c");
        rule.commands := [] (applyMacros(makeData.macros, "$(CC) -c $(CPPFLAGS) $(CFLAGS) $< -o $@", TRUE));
        addPatternRule(makeData, rule);
      end if;
    end if;
    if not patternRulePresent(makeData, "%.o", "%.cpp") then
      if ".cpp.o" in makeData.rules then
        rule.target := "%.o";
        rule.dependencies := [] ("%.cpp");
        rule.commands := makeData.rules[".cpp.o"].commands;
        addPatternRule(makeData, rule);
        excl(makeData.rules, ".cpp.o");
      else
        rule.target := "%.o";
        rule.dependencies := [] ("%.cpp");
        rule.commands := [] (applyMacros(makeData.macros, "$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $< -o $@", TRUE));
        addPatternRule(makeData, rule);
      end if;
    end if;
    if length(targets) = 0 then
      processRule(makeData, makeData.targetOfFirstRule);
    else
      for target range targets do
        processRule(makeData, target);
      end for;
    end if;
  end func;


(**
 *  Use rules and commands from a makefile to create the specified ''targets''.
 *  @param makefile Path of the makefile which contains rules and commands
 *                  in the usual makefile syntax.
 *  @param targets The targets that should be made.
 *  @param flags Options which affect command execution
 *  @exception FILE_ERROR A command failed and the flag ignoreErrors
 *                        has not been set (ignoreErrors not in flags holds).
 *)
const proc: make (in string: makefile, in array string: targets, in makeFlags: flags) is func
  begin
    make(makefile, targets, flags, stringHash.value);
  end func;


(**
 *  Use rules and commands from a makefile to create the specified ''target''.
 *  @param makefile Path of the makefile which contains rules and commands
 *                  in the usual makefile syntax.
 *  @param target The target that should be made.
 *  @param flags Options which affect command execution
 *  @param macros Predefined make macros.
 *  @exception FILE_ERROR A command failed and the flag ignoreErrors
 *                        has not been set (ignoreErrors not in flags holds).
 *)
const proc: make (in string: makefile, in string: target, in makeFlags: flags, in stringHash: macros) is func
  begin
    if target = "" then
      make(makefile, 0 times "", flags, macros);
    else
      make(makefile, [] (target), flags, macros);
    end if;
  end func;


(**
 *  Use rules and commands from a makefile to create the specified ''target''.
 *  @param makefile Path of the makefile which contains rules and commands
 *                  in the usual makefile syntax.
 *  @param target The target that should be made.
 *  @param flags Options which affect command execution
 *  @exception FILE_ERROR A command failed and the flag ignoreErrors
 *                        has not been set (ignoreErrors not in flags holds).
 *)
const proc: make (in string: makefile, in string: target, in makeFlags: flags) is func
  begin
    make(makefile, target, flags, stringHash.value);
  end func;