<?php if (!defined('PmWiki')) exit();
/**
  Reminder for PmWiki
  Copyright 2008-2025 Petko Yotov pmwiki.org/petko

  This text is written for PmWiki; 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 3 of the License, or
  (at your option) any later version. See pmwiki.php for full details
  and lack of warranty.

*/
# Version date
$RecipeInfo['Reminder']['Version'] = '20250208a';

Markup('reminder', '<nl1',
  '/\\(:rem\\s*(.*?):\\)(.*?)\\(:remend:\\)/is',
  "mu_reminder");
Markup('reminder-ext', '<reminder',
  '/\\(:reminder\\s*(.*?):\\)/is',
  "mu_reminder");

function mu_reminder($m) {
  extract($GLOBALS['MarkupToHTML']);
  PRR();
  if(!@$m[2]) $m[2] = '';
  return FmtReminder($pagename,$m[1],$m[2]);
}

SDV($ReminderPastDays, 3);
SDV($ReminderFutureDays, 7);
SDV($ReminderLocale, '');;
SDV($ReminderWeeklyFix, 0);
SDV($ReminderDayMonth, 'd/m');
SDV($ReminderDateFormat, '%A, %d %B %Y');
SDV($EnableReminderEmptyDays, 1);
SDV($EnableReminderDiff, 1);
SDV($EnableReminderRefresh, 0);
SDV($ReminderFmt, "%s <span class='remdef'>[%s]</span>");
SDV($ReminderLogKeyword, 'Reminder');
SDV($ReminderIncludeSuffix, '');
SDVA($ReminderTable, array(
  "W"=>120,
  "H"=>120,
  "M"=>1,
  "B"=>2,
));

SDVA($ReminderSPat,
  array("/^(\\*|[:]?{$ReminderLogKeyword}[:])\\s*(=|~)(w?\\d\\d?)(?:\\/(\\d\\d?))?(?:\\/(\\d{4}))?\\s/m"
  =>'mu_remindernext' ));

function mu_remindernext($m) {
  return ReminderNext($m[1].' '.$m[2], $m[3], $m[4], $m[5]);
}
  
SDV($RemindersLogs, array("\$SiteGroup.AllReminders"));

$MarkupExpr['remtoday'] = 'Keep(str_replace("\'", "&#39;", FmtReminder($args[0],
  "$args[0] today=$args[1] pastdays=0 futuredays=0 dayheaders=0 liststyle=0" ) ))';

function FmtToSec($f, $mtply=1) {
  global $Now;
  return intval(ltrim(PSFT($f, $Now), '0'))*$mtply;
}

function EasterJD($y) {
  $western = gregoriantojd(3, 21, $y);
  $eastern = juliantojd(3, 21, $y);
  $wadd = easter_days($y, CAL_EASTER_ALWAYS_GREGORIAN);
  $eadd = easter_days($y, CAL_EASTER_ALWAYS_JULIAN);
  return array('W'=>$western+$wadd, 'E'=>$eastern+$eadd);
}

function NthWeekdayMonthYear($nth, $dow, $month, $year) {
  if($nth>=1) {
    $first = strtotime("$year-$month-01");
    $fdow = date('w', $first);
    $offset = ($dow - $fdow + 7) % 7;
    $firstweekday = strtotime("+$offset days", $first);
  }
  else {
    $last = strtotime("last day of $year-$month");
    $ldow = date('w', $last);
    $offset = ($ldow - $dow + 7) % 7;
    $lastweekday = strtotime("-$offset days", $last);
  }
  
  
  
  if($nth == 1) $wday = $firstweekday;
  elseif($nth == 0) $wday = $lastweekday;
  elseif($nth>1) {
    $wday = strtotime("+" . ($nth-1) . " weeks", $firstweekday);
  }
  elseif($nth<0) {
    $wday = strtotime("$nth weeks", $lastweekday);
  }
  
  return PSFT('%m-%d', $wday);
}

function FmtReminder($pagename, $args, $content='') {
  global $Now, $InputValues, $ReminderPastDays, $ReminderFutureDays, $ReminderTable, $ReminderFmt,
    $ReminderLocale, $ReminderDayMonth, $ReminderDateFormat, $ReminderIncludeSuffix, $HTMLStylesFmt,
    $EnableReminderRefresh, $EnableReminderEmptyDays, $SiteAllReminders, $QualifyPatterns, $HTMLHeaderFmt,
    $ReminderWeeklyFix;
  if($EnableReminderRefresh) {
    $sec = 24*3600 - (FmtToSec('%H', 3600) + FmtToSec('%M', 60) + FmtToSec('%S'))+300;
    $HTMLHeaderFmt['refresh-reminder'] = "<meta http-equiv='refresh' content='$sec'>";
  }

  $opt = ParseArgs($args);
  
  $TH = IsEnabled($opt['th'], $ReminderTable["H"]);
  $TW = IsEnabled($opt['tw'], $ReminderTable['W']);
  $TM = IsEnabled($opt['tm'], $ReminderTable['M']);
  $TB = IsEnabled($opt['tb'], $ReminderTable['B']);
  
  $drur = "div.remtable ul.reminder";
  $HTMLStylesFmt['remtable'] = "$drur { font-size: 90%; line-height: 1.1em; }
    $drur li.date { position: relative; display: block; float: left; overflow: hidden;
      height: {$TH}px; width: {$TW}px; border: {$TB}px solid #ddd; margin-left: {$TM}px; }
    $drur li.date:hover { overflow: visible;  z-index: 50;}
    $drur li.today { border: {$TB}px solid #a00; font-size: inherit; }
    $drur li.empty { border-color: #fff; background-color: #fff;}\n";
  if(isset($opt['weeks'])) $HTMLStylesFmt['remtable'] .= "$drur li.w{$opt['weeks']} { clear: left;}\n";
  $Tsptintf = "$drur li.w%d:first-child, $drur li.overdue + li.w%d {margin-left: %dpx;}\n";
  for($i=1; $i<8; $i++) {
    $j = $i%7;
    $k = $i-1;
    $margin = $k*($TW+2*$TB+$TM);
   // $HTMLStylesFmt['remtable'] .= sprintf($Tsptintf, $j, $j, $margin);
  }
 

  $TodayNow =  IsEnabled($opt['today'], PSFT('%Y-%m-%d', $Now));
  $TodayNow2 = IsEnabled($opt['today'], PSFT('%Y%m%d', $Now));
  if(! strpos($TodayNow, '-') ) $TodayNow = PSFT('%Y-%m-%d', strtotime($TodayNow) );

  $dayheaders = IsEnabled($opt['dayheaders'], 1);
  $liststyle = IsEnabled($opt['liststyle'], 1);
  $expired = IsEnabled($opt['expired'], 1);

  
  $pastdays = IsEnabled($opt['pastdays'], $ReminderPastDays);
  $futuredays = IsEnabled($opt['futuredays'], $ReminderFutureDays);
  $locale = IsEnabled($opt['locale'], $ReminderLocale);
  $datefmt = IsEnabled($opt['datefmt'], $ReminderDateFormat);
  $emptydays = (@$opt['emptydays']>'')? $opt['emptydays'] : $EnableReminderEmptyDays;
  $df = explode('/', $ReminderDayMonth);
  $onemonth = intval(IsEnabled($opt['onemonth'], @$_REQUEST['onemonth']));
  
  
  $CurrentYear = intval(date('Y'));
  $NextYear = $CurrentYear+1;
  $easterdays = array();
  for($i=0; $i<3; $i++) {
    $easterdays[$i] = EasterJD($CurrentYear+$i);
  }
  $julianday = unixtojd();
  
  
  

  if(@$opt[''][0]>'')$content .= "\n". IncludeText($pagename, $opt[''][0]);
  
  if($locale)setlocale(LC_TIME, $locale);

  $_DATES = array();
  $lines = explode("\n", $content);
  $out = "";
  
  if($ReminderIncludeSuffix) {
    $QP = array('/(REM)?$/m'=>'$1'.$ReminderIncludeSuffix) +$QualifyPatterns;
    $QualifyPatterns = $QP;
  }
  $restart=1;
  while($restart) {
    $restart=0;
    foreach($lines as $i=>$line)
      if(preg_match("/^#include (.*?)$/", $line, $m)) {
        array_splice($lines, count($lines), 0, explode("\n", IncludeText($pagename, PSS($m[1]))));
        $lines[$i]='';
        $restart=1;
      }
  }
  if($ReminderIncludeSuffix) unset($QualifyPatterns['/(REM)?$/m']);

  foreach($lines as $line) {
    if(@$line[0]!='*') continue;
    $line = preg_replace_callback('/\\{(\\*|!?[-\\w.\\/\\x80-\\xff]*)(\\$:?\\w[-\\w]*)\\}/', "MarkupPageVar", $line);
    $line = preg_replace_callback('/\\{(\\(\\w+\\b.*?\\))\\}/', "MarkupMarkupExpression", $line);
    @list($when, $what) = explode(' ', preg_replace("/^\\* */", '', $line), 2);

    $md5 = md5($line);

    $time = 1500; // 25*60
    $iso = false;
    
    $lwhat = strtolower($what);
    preg_match('/\\b(?P<hour>[012]?\\d)[h:](?P<min>[0-5]\\d)?(?: *(?P<ampm>[ap])m)?\\b/i', $lwhat, $m);
    if(!$m) preg_match('/\\b(?P<hour>[012]?\\d)(?P<ampm>[ap])m\\b/i', $lwhat, $m);
    
    if($m) {
      $h = intval($m['hour']);
      $time = 60*$h+intval(@$m['min']);
      if(@$m['ampm']=='p' && $h<12) $time += 720;
      elseif (@$m['ampm']=='a' && $h==12) $time -= 720;
    }
    
    # Weekly
    if( preg_match('/^w(\\d+)$/i', $when, $m))
      for($i=0; $i<strlen($m[1]); $i++) {
        $dow = ($m[1][$i]+7-$ReminderWeeklyFix)%7;
        $_DATES[ 'w'. $dow ][] =
          array('what'=>sprintf($ReminderFmt, $what, $when, $md5), 'time'=>$time);
      }
  
    # Monthly
    elseif(preg_match('/^(-?\\d+)$/i', $when, $m)&&$m[1]<32)
      $_DATES["m".intval($m[1])][] =
        array('what'=>sprintf($ReminderFmt, $what, $when, $md5), 'time'=>$time);

    # Sticky
    elseif( preg_match('/^(=|~)(\\d{4}-(\\d\\d-\\d\\d))$/i', $when, $iso)) {
      if($iso[2]< $TodayNow && $iso[1] == '=' ) {
        $_DATES['overdue'][] = sprintf($ReminderFmt, $what, $when, $md5);
      }
      else {
        $_DATES[ $iso[3] ][] =
          array('what'=>sprintf($ReminderFmt, $what, $when, $md5), 'time'=>$time);
      }
    }
    
    # Yearly reminders
    elseif(preg_match('/^(\\d+)\\/(\\d+)(?:\\/(\\d+))?$/i', $when, $m)
      || preg_match('/^(\\d{4})-?(\\d\\d)-?(\\d\\d)$/i', $when, $iso)) {
      if(@$iso[0]) {
        $x = array('m'=>intval($iso[2]), 'd'=>intval($iso[3]));
        $m = array(3=>$iso[1]);
      }
      else
        $x=array($df[0]=>intval($m[1]),$df[1]=>intval($m[2]));
      $st = PSFT('%m-%d', strtotime("2000-{$x['m']}-{$x['d']}"));
      $st2 = sprintf("%02d-%02d", $x['m'], $x['d']);
      if($st!=$st2)
        $_DATES['unknown'][] = sprintf($ReminderFmt, $what, $when, $md5);
      else
        $_DATES[ $st ][] =
          array('y'=>@$m[3],'what'=>sprintf($ReminderFmt, $what, $when, $md5), 'time'=>$time);
    }
    
    # Start date, custom interval in days, weeks
    elseif(preg_match('/^(\\d+)\\/(\\d+)\\/(\\d+)r(\\d+)([dw]?)$/i', $when, $n)) {
      $m = array_map('intval', $n);
      
      $x=array($df[0]=>$m[1], $df[1]=>$m[2], 'y'=>$m[3]);
      
      $jdstart = unixtojd(strtotime("{$x['y']}-{$x['m']}-{$x['d']}"));
      
      $rdays = $m[4];
      if($n[5]=='w') $rdays*=7;
        
      $_DATES[ 'custominterval' ][] = array(
        'start'=>$jdstart, 
        'interval'=>$rdays,
        'what'=>sprintf($ReminderFmt, $what, $when, $md5), 
        'time'=>$time);
    }

    # Multiple days DateStart..DateEnd
    elseif(preg_match('/^(\\d{1,2})\\/(\\d{1,2})(?:\\/(\\d{4}))?\\.\\.(\\d{1,2})\\/(\\d{1,2})(?:\\/(\\d{4}))?(?:([mw])(\\d+))?$/i', $when, $m)) {
      $x=array($df[0]=>intval($m[1]),$df[1]=>intval($m[2]));
      $x2=array($df[0]=>intval($m[4]),$df[1]=>intval($m[5]));
      $y2 = @$m[6]? $m[6] : date('Y');
      $y1 = @$m[3]? $m[3] : $y2;
      $T1 = strtotime("$y1-{$x['m']}-{$x['d']}");
      $T2 = strtotime("$y2-{$x2['m']}-{$x2['d']}");
      if($T1>$T2) {  
        if(@$m[6] || $x2['m']>2)
          $T1 = strtotime( ($y2-1) ."-{$x['m']}-{$x['d']}");
        else
          $T2 = strtotime( ($y2+1) ."-{$x2['m']}-{$x2['d']}");
      }

      if( PSFT('%m-%d', $T1)!=sprintf("%02d-%02d", $x['m'], $x['d'])
        || PSFT('%m-%d', $T2)!=sprintf("%02d-%02d", $x2['m'], $x2['d'])) {
        $_DATES['unknown'][] = "$what [$when]  "
          .PSFT('%m-%d', $T1).'!='.sprintf("%02d-%02d", $x['m'], $x['d'])
          ."  "
          .PSFT('%m-%d', $T2).'!='.sprintf("%02d-%02d", $x2['m'], $x2['d']);
        continue;
      }
      if(@$m[6] && PSFT('%Y-%m-%d', $T2)< $TodayNow) {
        $_DATES['overdue'][] = sprintf($ReminderFmt, $what, $when, $md5);
        continue;
      }

      $T0 = 0;
      
      $interval = @$m[7];
      if($interval) {
        $intdate = intval($m[8]);
        $intday = [];
        for($i=0;$i<strlen($m[8]);$i++) {
          $mod = (intval($m[8][$i])+7-$ReminderWeeklyFix)%7;
          $intday[$mod] = 1;
        }
      }
      
      for($i=0; $T0<$T2; $i++) {
        $T0 = strtotime("+$i days", $T1);
        $st = PSFT('%Y-%m-%d', $T0);
        $Mtime = $time==1500? 0: $time+1;
        if($interval == 'w') {
          $dow = intval(PSFT('%w', $T0));
          if(isset($intday[$dow]))
            $_DATES[ "w$st" ][] =
              array('what'=>sprintf($ReminderFmt, $what, $when, $md5), 'time'=>$Mtime);
          continue;
        }
        if($interval == 'm') {
          if(intval(PSFT('%d', $T0)) == $intdate)
            $_DATES[ "m$st" ][] =
              array('what'=>sprintf($ReminderFmt, $what, $when, $md5), 'time'=>$Mtime);
          continue;
        }
        $_DATES[ "M$st" ][] =
          array('what'=>sprintf($ReminderFmt, $what, $when, $md5), 'time'=>$Mtime);
      }
    }
    
    
    # Easter
    elseif(preg_match('/^([WE])E([-+]\\d+)([dw]?)$/', $when, $m)) {
      $days = intval($m[2]);
      if($m[3]=='w') $days *= 7;
      for($i=0; $i<count($easterdays); $i++) {
        $jd = $easterdays[$i][$m[1]]+$days;
        list($n, $d) = explode('/', jdtogregorian($jd));
        $st = sprintf('%02d-%02d', $n, $d);
        $_DATES[$st][] = array(
          'what'=>sprintf($ReminderFmt, $what, $when, $md5), 
          'time'=>$time, 
          'year'=>$CurrentYear+$i
        );
      }
    }
    
    # Nth Weekday of Month 4w4@11 Thanksgiving
    elseif(preg_match('/^(-?[012345])w([0-7])@(0?[1-9]|1[012])$/', $when, $m)) {
      $n = array_map('intval', $m);
      $dow = ($n[2]+7-$ReminderWeeklyFix)%7;
      for($i=0; $i<3; $i++) {
        $st = NthWeekdayMonthYear($n[1], $dow, $n[3], $CurrentYear+$i);
        $_DATES[$st][] = array(
          'what'=>sprintf($ReminderFmt, $what, $when, $md5), 
          'time'=>$time, 
          'year'=>$CurrentYear+$i
        );
      }
      
    }

    else $_DATES['unknown'][] = sprintf($ReminderFmt, $what, $when, $md5);
  }
  
  if($expired && isset($_DATES['overdue']) && count($_DATES['overdue']) ) {
    $out.="\n* %list reminder% %item date overdue% $[Expired]\n";
    foreach($_DATES['overdue'] as $v)$out.="** $v\n";
  }

  if($onemonth && $onemonth<13) {
    $y = $onemonth<date('n')?date('Y')+1:date('Y');
    $Then = strtotime("$y-$onemonth-01");
    $maxdays = date('t', $Then);
    
    if(isset($opt['weeks'])) {
      $ThenDOW = date('w', $Then);
      for($i=$opt['weeks']; $i<$ThenDOW; $i++) {
        $out .= "*  %list reminder% %item date empty% &nbsp; \n";
      }
    }
    for($i=0; $i<$maxdays; $i++) {
      $T = strtotime("+$i days", $Then);
      $out .= ReminderOneDay($T, $_DATES, $datefmt, $emptydays);
    }
  }
  else {
    if(isset($opt['weeks'])) {
      $ThenDOW = date('w', strtotime("-$pastdays days", strtotime($TodayNow)));
      $pastdays+= $ThenDOW-$opt['weeks'];
    }
    for($i=-$pastdays; $i<=$futuredays; $i++) {
      $plus = $i<0?'':'+';
      $T = strtotime("$plus$i days", strtotime($TodayNow));
      $out .= ReminderOneDay($T, $_DATES, $datefmt, $emptydays, $dayheaders, $liststyle, $expired);
    }
  }
  if(isset($_DATES['unknown']) && count($_DATES['unknown']) ) {
    $out.="\n* %item date unknown% $[Dates unknown]\n";
    foreach($_DATES['unknown'] as $v)$out.="** $v\n";
  }
  return $out;
}



function ReminderOneDay($T, $_DATES, $datefmt, $emptydays=1, $dayheaders=1, $liststyle=1, $expired=1) {
  global $Now, $PageUrl, $EnableReminderDiff;
  $st = PSFT('%m-%d', $T);
  $d = date('j', $T);
  $maxdays = date('t', $T);
  $D = $d-$maxdays;
  $y = date('Y', $T);
  $w = date('w', $T);
  
  if("$y-$st"==date('Y-m-d', $Now)) $class='today';
  elseif($T<$Now)$class='past';
  else $class='future';
  $loop = array(
    array('multiple', "M$y-$st"),
    array('yearly', $st),
    array('monthly', "m$d"),
    array('monthly', "m$D"),
    array('monthly range', "m$y-$st"),
    array('weekly', "w$w"),
    array('weekly range', "w$y-$st"),
  );

  $ret = '';
  $out = array();
  foreach($loop as $array) {
    list($cname,$n)= $array;
    if( isset($_DATES[$n]) ) foreach($_DATES[$n] as $a) {
      if(isset($a['year']) && $a['year'] != $y) continue; # for Easter dates
      $yDiff = (IsEnabled($EnableReminderDiff, 1) && $cname=='yearly'&&@$a['y']) 
        ? ' ('.($y-$a['y']).')'  : '';
      $out["** %item $cname% {$a['what']}$yDiff\n"] = $a['time'];
    }
  }
  
  if(isset($_DATES['custominterval'])) {
    $jday = unixtojd($T);
    
    foreach($_DATES['custominterval'] as $a) {
      if($a['start']>$jday) continue;
      $jdiff = $jday - $a['start'];
      
      if($jdiff % $a['interval'] === 0) {
        $out["** %item custom% {$a['what']}\n"] = $a['time'];
      }
    
    }
  }
  
  if($out) {
    asort($out);
    $ret = implode('', array_keys($out));
  }
  if(!$liststyle) $ret = preg_replace("/%(list|item).*?%/", '', $ret);
  if($ret>'' || $emptydays)
    return ($dayheaders? "* %list reminder% %item date $class w$w%"
    . PSFT($datefmt, $T). "\n":'') .$ret;
}

array_unshift($EditFunctions, "ReminderLogAll");
function ReminderLogAll($pagename,&$page,&$new) {
  global $EnablePost, $RecentChangesFmt, $RemindersLogs, $ReminderSPat, $ReminderLogKeyword;
  if (!$EnablePost) return;
  $new['text'] = PPRA($ReminderSPat, $new['text']);

  if (!@$RemindersLogs[0]) return;

  $pslash = str_replace('.', '/', $pagename);
  $pslashE = preg_quote("[[$pslash]]");
  $oldmatch = preg_match("/^:?{$ReminderLogKeyword}:.*$/m", strval(@$page['text']));
  $newmatch = preg_match_all("/^:?{$ReminderLogKeyword}:(.*)$/m", $new['text'], $m, PREG_PATTERN_ORDER);
  if( $oldmatch || $newmatch ) {
    foreach((array)$RemindersLogs as $p) {
      $rcpage = ReadPage($p);
      if($oldmatch)
        $rcpage['text'] = preg_replace("!^\\*.*  {$pslashE}\n!m", '', @$rcpage['text']);
      if($newmatch)
        foreach($m[1] as $v)
          $rcpage['text'] .= "* ". trim($v). " . .  [[$pslash]]\n";
      WritePage($p, $rcpage);
    }
  }
}

function ReminderNext($prefix, $a, $b=0, $y=0) {
  global $ReminderDayMonth, $Now;
  if($a[0]=='w') {
    $days = explode(' ', 'Sun Mon Tues Wednes Thurs Fri Satur');
    $day = $days[ $a[1]%7 ];
    return $prefix.PSFT("%Y-%m-%d", strtotime("{$day}day") )." ";
  }
  $df = explode('/', $ReminderDayMonth);
  $x[$df[0]]=$a;
  $x[$df[1]]=$b;
  if($a && $b && $y)
    return $prefix.PSFT("%Y-%m-%d", strtotime("$y-{$x['m']}-{$x['d']}") )." ";
  if($b==0) {
    $x['d'] = $a;
    $x['m'] = 0;
  }
  $delta = "years";
  if($x['m']==0){$delta = "months"; $x['m'] = date('n');}
  if($y==0)$y =date('Y');
  $Today = strtotime( PSFT("%Y-%m-%d"));
  $Then = strtotime("$y-{$x['m']}-{$x['d']}");
  
  for($i=0; $i<13; $i++) {
    $Current = strtotime( "+$i $delta", $Then );
    if($Today<=$Current)
      return $prefix.PSFT("%Y-%m-%d", $Current)." ";
  }
}