
(*  Copyright 2004 Sascha Husa, Ian Hinder, Christiane Lechner

    This file is part of Kranc.

    Kranc is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    Kranc 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Kranc; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*)

BeginPackage["Schedule`", {"Thorn`", "KrancGroups`", "MapLookup`", "Errors`", "Helpers`", "Kranc`", "CaKernel`", "Calculation`", "ParamCheck`", "CodeGenSchedule`"}];

CreateKrancScheduleFile;

Begin["`Private`"];

(* --------------------------------------------------------------------------
   Scheduling
   -------------------------------------------------------------------------- *)

storageStructure[groupName_, timelevels_] := 
{
  KrancGroup -> groupName,
  Timelevels -> timelevels
};

groupsSetInCalc[calc_, groups_] :=
  Module[{gfsInLHS, lhsGroupNames},
    gfsInLHS = variablesSetInCalc[calc, groups];
    lhsGroupNames = containingGroups[gfsInLHS, groups];
    Return[lhsGroupNames]
  ];

groupsReadInCalc[calc_, groups_] :=
  Module[{gfsInRHS, rhsGroupNames},
    gfsInRHS = variablesReadInCalc[calc, groups];
    rhsGroupNames = containingGroups[gfsInRHS, groups];
    Return[rhsGroupNames]
  ];

variablesSetInCalc[calc_, groups_] :=
  Module[{gfs, eqs, lhss, gfsInLHS},
    gfs = allGroupVariables[groups];
    eqs = lookup[calc, Equations];
    lhss = Map[First, eqs];
    gfsInLHS = Union[Cases[lhss, _ ? (MemberQ[gfs,#] &), Infinity]];
    Return[gfsInLHS]
  ];

variablesReadInCalc[calc_, groups_] :=
  Module[{eqsHaveVar, firstEqWithVar, eqHasVarInRHS,
          gfs, eqs, gfsInEqs, gfsInRHS},
    (* Look for the first equation in which the variable occurs in the
       calculation. If it is a RHS, then the variable is read. *)
    eqsHaveVar[eqs_, gf_] := MemberQ[eqs, gf, Infinity];
    firstEqWithVar[eqs_, gf_] := First[Select[eqs, MemberQ[#, gf, Infinity]&]];
    eqHasVarInRHS[eq_, gf_] := MemberQ[{Last[eq]}, gf, Infinity];
    gfs = allGroupVariables[groups];
    eqs = lookup[calc, Equations];
    gfsInEqs = Select[gfs, eqsHaveVar[eqs, #]&];
    gfsInRHS = Select[gfsInEqs, eqHasVarInRHS[firstEqWithVar[eqs, #], #]&];
    Return[gfsInRHS]
  ];


(* Each calculation can be scheduled at multiple points, so this
   function returns a LIST of schedule structures for each calculation
   *)

Options[scheduleCalc] = ThornOptions;
scheduleCalc[calc_, groups_, thornName_, OptionsPattern[]] :=
  Module[{points, conditional, conditionals, keywordConditional,
          keywordConditionals, triggered, keyword, value, keywordvaluepairs,
          groupsToSync, tags,
          prefixWithScope, groupsToRequire, groupsToProvide,
          groupName, userSchedule, groupSched, fnSched,
          selbcSched, appbcSched, bcGroupName, condParams, bcGroupSched, before, after, relStr,
          variablesToRead, variablesToWrite},
    conditional = mapContains[calc, ConditionalOnKeyword];
    conditionals = mapContains[calc, ConditionalOnKeywords];
    triggered = mapContains[calc, TriggerGroups];
    If[conditional,
      keywordConditional = lookup[calc, ConditionalOnKeyword];
      If[! MatchQ[keywordConditional, {lhs_String, rhs_String}],
        ThrowError["ConditionalOnKeyword entry in calculation expected to be of the form {parameter, value}, but was ", keywordConditional, "Calculation is ", calc]];

      keyword = keywordConditional[[1]];
      value = keywordConditional[[2]];
      ];
    If[conditionals,
      keywordConditionals = lookup[calc, ConditionalOnKeywords];
      If[! MatchQ[keywordConditionals, {{_, _} ...}],
        ThrowError["ConditionalOnKeywords entry in calculation expected to be of the form {{parameter, value}}, but was ", keywordConditionals, "Calculation is ", calc]];

      keywordvaluepairs =
        Map[# /. {keyword_, value_} -> {Parameter -> keyword, Value -> value} &,
            keywordConditionals];
      ];

    groupsToSync = If[lookupDefault[calc, Where, Everywhere] === Interior || 
                      lookupDefault[calc, Where, Everywhere] === Boundary,
                      groupsSetInCalc[calc, groups],
                      {}];

    (* TODO: Pass this as {keyword,value} pair instead of a string,
       once Thorn.m understands this format *)
    tags = If[OptionValue[UseOpenCL] || CalculationOnDevice[calc], "Device=1", ""];
    
    prefixWithScope[group_] :=
      If[StringMatchQ[ToString[group], __~~"::"~~__],
         ToString[group],
         thornName <> "::" <> ToString[group]];
    (* groupsToRequire = prefixWithScope /@ groupsReadInCalc[calc, groups]; *)
    (* groupsToProvide = prefixWithScope /@ groupsSetInCalc[calc, groups]; *)

    variablesToRead = qualifyGFName[#, groups, thornName] & /@ variablesReadInCalc[calc, groups];
    variablesToWrite = qualifyGFName[#, groups, thornName] & /@ variablesSetInCalc[calc, groups];


    before = lookupDefault[calc, Before, None];
    after = lookupDefault[calc, After, None];

    relStr = If[before =!= None, " before " <> before, ""]
             <> If[after =!= None, " after " <> after, ""];

    applyBCs = lookupDefault[calc, ApplyBCs, False];
    userSchedule = GetSchedule[calc];


    If[userSchedule =!= Automatic && !applyBCs,
    Return[Map[
      Join[
      {
        Name               -> GetCalculationScheduleName[calc]<>If[GetCalculationScheduleName[calc] =!= GetCalculationName[calc], " as "<>GetCalculationName[calc],""],
        SchedulePoint      -> # <> relStr,
        SynchronizedGroups -> If[StringMatchQ[#, "*MoL_CalcRHS*", IgnoreCase -> True] || StringMatchQ[#, "*MoL_RHSBoundaries*", IgnoreCase -> True],
                                 {},
                                 groupsToSync],
        Language           -> CodeGenC`SOURCELANGUAGE, 
        Tags               -> tags,
        RequiredGroups     -> variablesToRead,
        RequiredRegion     -> Everywhere,   (* TODO: be more accurate *)
        ProvidedGroups     -> variablesToWrite,
        ProvidedRegion     -> lookupDefault[calc, Where, Everywhere],
        Comment            -> lookup[calc, Name]
      },
       If[triggered, {TriggerGroups -> lookup[calc, TriggerGroups]},
          {}],
       If[conditional, {Conditional -> {Parameter -> keyword, Value -> value}},
          {}],
       If[conditionals, {Conditionals -> keywordvaluepairs},
          {}],
        If[mapContains[calc, Conditional], {NewConditional -> lookup[calc,Conditional]}, {}]
      ] &,
      GetSchedule[calc]]],

      (* Scheduling is automatic.  For the moment, all automatically
      scheduled functions are going to be performed in
      MoL_PseudoEvolution in a new group, along with routines to apply
      boundary conditions to the variables set in the calculation. All
      variables set in the calculation will be synchronised. *)

      groupName = lookup[calc, Name] <> "_group";
      bcGroupName = lookup[calc, Name] <> "_bc_group";

      condParams = Join[
        If[conditional,
           {Conditional -> {Parameter -> keyword, Value -> value}}, {}],
        If[conditionals, {Conditionals -> keywordvaluepairs}, {}]];

      groupSched = {
        Name               -> "group " <> groupName,
        SchedulePoint      -> If[applyBCs, First[userSchedule] <> relStr, "in MoL_PseudoEvolution" <> relStr],
        SynchronizedGroups -> {},
        Language           -> "None",
        Comment            -> lookup[calc, Name]
      }
      ~Join~ condParams;

      (* We set required/provided groups here with the actual
         function, since (at least in principle) the driver should be
         able to deduce the synchronization and boundary condition
         treatment from this information. *)
      fnSched = {
        Name               -> lookup[calc, Name],
        SchedulePoint      -> "in " <> groupName,
        Language           -> CodeGenC`SOURCELANGUAGE,
        Tags               -> tags,
        RequiredGroups     -> variablesToRead,
        RequiredRegion     -> Everywhere,   (* TODO: be more accurate *)
        ProvidedGroups     -> variablesToWrite,
        ProvidedRegion     -> Interior, (* since we apply boundary conditions *)
        Comment            -> lookup[calc, Name]
      };

      bcGroupSched[where_] := {
        Name               -> "group " <> bcGroupName,
        SchedulePoint      -> where,
        SynchronizedGroups -> {},
        Language           -> "None",
        Comment            -> lookup[calc, Name]
      }
      ~Join~ condParams;

      selbcSched = {
        Name               -> lookup[calc, Name] <> "_SelectBCs",
        SchedulePoint      -> "in " <> bcGroupName,
        SynchronizedGroups -> groupsToSync,
        Options               -> "level",
        Language           -> CodeGenC`SOURCELANGUAGE,
        Comment            -> lookup[calc, Name] <> "_SelectBCs"
      };

      appbcSched = {
        Name               -> "group ApplyBCs as " <> lookup[calc,Name] <> "_ApplyBCs",
        SchedulePoint      -> "in " <> bcGroupName <> " after " <> lookup[calc, Name] <> "_SelectBCs",
        Language           -> "None",
        Comment            -> "Apply BCs for groups set in " <> lookup[calc, Name]
      };

      Return[{groupSched, fnSched} ~Join~ If[groupsToSync =!= {},
        {selbcSched, appbcSched,
         bcGroupSched["in "<>groupName <> " after " <> lookup[calc, Name]],
         bcGroupSched["in MoL_PseudoEvolutionBoundaries after MoL_PostStep"]},{}]]]];

Options[CreateKrancScheduleFile] = ThornOptions;
CreateKrancScheduleFile[calcs_, declaredGroups_, groups_, thornName_, 
                        evolutionTimelevels_, opts:OptionsPattern[]] :=
  Module[{scheduledCalcs, scheduledStartup, scheduleMoLRegister, globalStorageGroups, scheduledFunctions, schedule, allParams, calcGroups, scheduleTimelevels},

    scheduledCalcs = Flatten[Map[scheduleCalc[#, groups, thornName, opts] &, calcs], 1];
    scheduledStartup = 
    {
      Name          -> thornName <> "_Startup",
      SchedulePoint -> "at STARTUP",
      Language      -> "C",
      Options       -> "meta",
      Comment       -> "create banner"
    };

    scheduleMoLRegister =
    {
      Name          -> thornName <> "_RegisterVars",
      SchedulePoint -> "in MoL_Register", 
      Language      -> "C", 
      Options       -> "meta",
      Comment       -> "Register Variables for MoL"
    };
    
    scheduleRegisterSymmetries =
    {
      Name          -> thornName <> "_RegisterSymmetries",
      SchedulePoint -> "in SymmetryRegister", 
      Language      -> "C",
      Options       -> "meta",
      Comment       -> "register symmetries"
    };

    scheduleTimelevels[gn_] :=
      Module[
        {groupDef, extras},
        groupDef = groupFromName[gn, groups];
        extras = GroupExtras[groupDef];
        lookup[extras, ScheduleTimelevels, 
               (* Nonevolved groups do not have a ScheduleTimelevels
                  extra; we use this old code for now *)
                 (* Number of timelevels requested for this group, or 1 if no request made *)
                 If[NonevolvedTimelevels[groupDef]===1,
                    "other_timelevels",
                    "timelevels"]]];

    globalStorageGroups = Map[storageStructure[#, scheduleTimelevels[#]] &, declaredGroups];

    (* Schedule groups defined in calculations *)
    calcGroups = Union[Flatten[Map[lookup[#, ScheduleGroups, {}] &, calcs],1]];

    scheduledFunctions = 
      Join[{scheduledStartup, scheduleRegisterSymmetries}, 
        scheduledCalcs, CactusBoundary`GetScheduledFunctions[thornName,
                                                             declaredGroups,
                                                             groups],
           {scheduleMoLRegister}, If[Length[OptionValue[ParameterConditions]] > 0,
                                     {ParameterCheckSchedule[thornName]},
                                     {}]];

    If[OptionValue[UseCaKernel],
       scheduledFunctions = Join[scheduledFunctions, CaKernelSchedule[thornName]]];

    allParams = Union@@((lookup[#,Parameters] &) /@ calcs);
    schedule = CreateSchedule[globalStorageGroups, 
      Join[CactusBoundary`GetScheduledGroups[thornName], calcGroups], scheduledFunctions, allParams];

    Return[schedule]];

End[];

EndPackage[];
