/*
 * EPROG Aufgabe #1106 (maskdate)
 * von Hannes Eder, e9521554@stud3.tuwien.ac.at
 *
 * Dokumentation siehe: doc/index.html, bzw. maskdate.txt
 */

import eprog.EprogIO;

import java.io.PrintStream;
import java.io.StringReader;
import java.text.NumberFormat;

/**
 * <b>Eprog Aufgabe #1106</b><br>
 * Autor: <b><a href="mailto:e9521554@student.tuwien.ac.at">Hannes Eder &lt;e9521554@student.tuwien.ac.at&gt;</a></b><br><br>
 *
 * siehe auch: <a href="../maskdate.html">Spezifikation</a><br><br>
 *
 * Um den Buildprozess zu vereinfachen, wurde ein <a href="../Makefile">Makefile</a> erstellt.<br><br>
 *
 * @version 1.0
 */
public class maskdate
{
  /**
   * wird <tt>java maskdate --debug</tt> ausgef&uuml;hrt, ist <tt>debug</tt> true, dadurch wird <tt>test_All</tt> aufgerufen
   *
   * @see maskdate#test_All
   */
  private static boolean debug = false;

  /** l&auml;uft irgendetwas falsch, dann wird dieser Text ausgegeben */
  final private static String msgFalscheEingabe = "FALSCHE EINGABE";

  /**
   * Main Entry Point.
   *
   * &Uuml;berpr&uuml;ft <tt>args</tt> auf <tt>--debug</tt>, f&uuml;hrt geg. falls einen Selbsttest
   * siehe <tt>test_All</tt> durch. Im Normalfall werden aber nur 2 W&ouml;rter &uuml;ber
   * <tt>EprogIO.readWord</tt> gelesen. <tt>maskDate</tt>
   * erzeugt den Output.
   *
   * @see maskdate#maskDate
   * @see maskdate#test_All
   *
   * @returns <tt>0</tt>kein Fehler bei der Verarbeitung, <tt>-1</tt>Selbsttest gescheitert
   */
  public static void main(String[] args)
  {
    int exitCode = 0;


    for ( int i=0; i < args.length; i++ )
    {
      if ( args[i].equals("--debug") )
        debug = true;
    }

    if (debug && (!test_All(System.err)))
    {
      // Selbsttest gescheitert
      System.err.println("[ERROR] Selbsttest gescheitert");
      exitCode = -1;
    } else
    {
      String date = EprogIO.readWord();
      String format = EprogIO.readWord();

      String output = maskDate(date, format);
      EprogIO.println(output);
    }

    System.exit(exitCode);
  }

  /**
   * F&uuml;hrt einen Selbsttest der wichtigsten Funktionen durch
   * @param log <tt>PrintStream</tt> auf dem das log ausgegeben werden soll, im Normalfall <tt>System.err</tt>
   * @return true, wenn der Test ok war.
   */
  private static boolean test_All(PrintStream log)
  {
    boolean bRet = true;

    bRet &= test_isLeapYear(log);
    bRet &= test_isDateValid(log);
    bRet &= test_checkFormat(log);
    bRet &= test_dayOfWeekOfFirstJanuary(log);
    bRet &= test_dayOfYear(log);
    bRet &= test_dayOfWeek(log);
    bRet &= test_formatDate(log);
    bRet &= test_maskDate(log);

    log.println((bRet ? "[OK]": "[ERR]") + " test_All");
    return bRet;
  }

  /**
   * @return true, wenn text dem Aufbau von format entspricht, sonst false
   */
  private static boolean checkFormat(String format, String text)
  {
    boolean bRet = true;

    if (format.length() != text.length())
    {
      bRet = false;
    } else
    {
      for (int i = 0; i < format.length(); i++)
      {
        char formatChar = format.charAt(i);
        char textChar = text.charAt(i);

        if ((formatChar == '#' && !Character.isDigit(textChar))
                || (formatChar != '#' && formatChar != textChar)
        )
        {
          bRet = false;
          break;
        }
      }
    }

    return bRet;
  }

  /**
   * Testet checkFormat mit ein paar repraestentativen Werten
   * @return true, wenn alle Test ok sind
   */
  private static boolean test_checkFormat(PrintStream log)
  {
    boolean bRet = true;

    bRet &= checkFormat("##/##/####", "12/34/5678") == true;
    bRet &= checkFormat("##-##-####", "99-99-9999") == true;
    bRet &= checkFormat("##/##/####", "1/2/1976") == false; // die Länge muss auch stimmen!
    bRet &= checkFormat("##/##/####", "12/12/12345") == false; // detto
    bRet &= checkFormat("##/##/####", "12.12/1999") == false;
    bRet &= checkFormat("##/##/####", "##/12/2001") == false;

    log.println((bRet ? "[OK]": "[ERR]") + " test_checkFormat");
    return bRet;
  }

  /**
   * @return true, wenn year, month und day ein gueltiges Datum bilden, year >= 1!
   */
  private static boolean isDateValid(int year, int month, int day)
  {
    boolean bRet = true;

    bRet &= (year >= 1);
    bRet &= (month >= 1) & (month <= 12);
    bRet &= (day >= 1) & (day <= 31);

    switch (month)
    {
      case 2:
        bRet &= (day <= 28 + (isLeapYear(year) ? 1 : 0));
        break;
      case 4:
      case 6:
      case 9:
      case 11:
        bRet &= (day <= 30);
        break;
    }

    return bRet;
  }

  /**
   * Testet die Funktion isDateValid mit repraestentativen Werten
   * @return true, wenn alle test ok waren
   */
  private static boolean test_isDateValid(PrintStream log)
  {
    boolean bRet = true;

    bRet &= isDateValid(0, 1, 1) == false;
    bRet &= isDateValid(1, 0, 1) == false;
    bRet &= isDateValid(1, 1, 1) == true;
    bRet &= isDateValid(1, 12, 31) == true;
    bRet &= isDateValid(1, 12, 32) == false; // wie die Werbung schon sagt, am 32.12 ist es zu spät
    bRet &= isDateValid(1, 13, 1) == false;
    bRet &= isDateValid(1976, 1, 2) == true; // mein Geburtstag ist klarerweise ein gueltiges Datum (c=
    bRet &= isDateValid(2000, 2, 29) == true;
    bRet &= isDateValid(2001, 2, 29) == false;
    bRet &= isDateValid(2001, 4, 31) == false;
    bRet &= isDateValid(2001, 6, 31) == false;
    bRet &= isDateValid(2001, 9, 31) == false;
    bRet &= isDateValid(2001, 11, 31) == false;

    log.println((bRet ? "[OK]": "[ERR]") + " test_isDateValid");
    return bRet;
  }

  /**
   * @return true, wenn year ein Schaltjahr ist
   */
  private static boolean isLeapYear(int year)
  {
    return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
  }

  /**
   * Ueberprueft die Funtkionalitaet von isLeapYear mit ein paar repraesentativen Werten
   * @return true, wenn alle Tests erfolgreich waren
   */
  private static boolean test_isLeapYear(PrintStream log)
  {
    boolean bRet = true;

    bRet &= isLeapYear(1900) == false;
    bRet &= isLeapYear(1996) == true;
    bRet &= isLeapYear(2000) == true;
    bRet &= isLeapYear(2001) == false;

    log.println((bRet ? "[OK]": "[ERR]") + " test_isLeapYear");
    return bRet;
  }

  /**
   * Berechnet den Wochentag des 1. Januar.
   *
   * Wochentag 1.1. = (Jahr + floor((Jahr-1)/4) - floor((Jahr-1)/100) + floor((Jahr-1)/400)) mod 7
   * wobei floor(x) für die größte ganze Zahl <= x steht.
   *
   * <b>Nicht bekannt ist für welchen Range diese Funktion gültig ist.</b>
   *
   * @return 0 für Sonntag, 1 für Montag, ... , 6 für Samstag.
   */
  private static int dayOfWeekOfFirstJanuary(int year)
  {
    return (year + (year - 1) / 4 - (year - 1) / 100 + (year - 1) / 400) % 7;
  }

  /**
   * Testet <tt>dayOfWeekOfFirstJanuary</tt>
   */
  private static boolean test_dayOfWeekOfFirstJanuary(PrintStream log)
  {
    boolean bRet = true;

    bRet &= dayOfWeekOfFirstJanuary(1999) == 5;
    bRet &= dayOfWeekOfFirstJanuary(1999) == 5;
    bRet &= dayOfWeekOfFirstJanuary(2000) == 6;
    bRet &= dayOfWeekOfFirstJanuary(2001) == 1;

    log.println((bRet ? "[OK]": "[ERR]") + " test_dayOfWeekOfFirstJanuary");
    return bRet;
  }

  /**
   * @throws DateInvalidExcpetion wenn das Datum ung&uuml;ltig ist
   * @returns Die nummer des Tages im Jahr, 1.1 = 1, 12.31.=365 bze. 366 in einem Schaltjahr
   */
  private static int dayOfYear(int year, int month, int day) throws DateInvalidExcpetion
  {
    int ret = 0;

    if (!isDateValid(year, month, day))
      throw new DateInvalidExcpetion(year, month, day);

    for (int i = 1; i < month; i++)
    {
      switch (i)
      {
        case 2:
          ret += 28 + (isLeapYear(year) ? 1 : 0);
          break;
        case 4:
        case 6:
        case 9:
        case 11:
          ret += 30;
          break;
        default:
          ret += 31;
          break;
      }
    }

    ret += day;

    return ret;
  }

  /**
   * Testet dayOfYear
   * @returns true, wenn der Test ok war
   */
  private static boolean test_dayOfYear(PrintStream log)
  {
    boolean bRet = true;

    try
    {
      bRet &= dayOfYear(2000, 1, 1) == 1;
      bRet &= dayOfYear(2000, 3, 1) == 31 + 29 + 1;
      bRet &= dayOfYear(2000, 12, 31) == 366;
      bRet &= dayOfYear(2001, 12, 31) == 365;
    }
    catch (Exception e)
    {
      bRet = false;
      log.println(e);
    }

    log.println((bRet ? "[OK]": "[ERR]") + " test_dayOfYear");
    return bRet;
  }

  /**
   * Berechnet den Wochentag zum geg. Datum.
   *
   * alternativ zu diesem Verfahren:<br>
   * - Algorithmus von Christian Zeller: Zeitschrift: Acta Mathematica Nr. 9 (1886/1887)<br>
   * - Algorithmus von J.D. Robertson: Collected Algorithms (CA) CACM 398 - R1<br>
   * <br>
   * eine weiter Moeglichkeit ist die Verwendung der Java Class java.util.GregorianCalendar<br>
   *
   * @return liefert den Wochentag zum geg. Datum, 0=So, 1=Mo, ... 6=Sa
   */
  private static int dayOfWeek(int year, int month, int day) throws Exception
  {
    if (!isDateValid(year, month, day))
      throw new DateInvalidExcpetion(year, month, day);

    // -1 weil zum 1.1. ja nichts mehr dazu gezählt werden muss
    return (dayOfWeekOfFirstJanuary(year) + dayOfYear(year, month, day) - 1) % 7;
  }

  /**
   * Testet dayOfWeek
   * @return true, wenn der Test ok war
   */
  private static boolean test_dayOfWeek(PrintStream log)
  {
    boolean bRet = true;

    try
    {
      bRet &= dayOfWeek(1976, 1, 2) == 5; // Fr, ich bin ein "Freitags"kind (c=
      bRet &= dayOfWeek(2001, 2, 1) == 4; // DO
      bRet &= dayOfWeek(2001, 12, 24) == 1; // MO
    }
    catch (Exception e)
    {
      bRet = false;
      log.println(e);
    }

    log.println((bRet ? "[OK]": "[ERR]") + " test_dayOfWeek");
    return bRet;
  }

  private static String dayOfWeekName(int dayOfWeek) throws Exception
  {
    final String[] daysOfWeek = { "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" };

    if (dayOfWeek < 0 || dayOfWeek > 6)
      throw new Exception("dayOfWeek=" + dayOfWeek + ", muss zwischen 0 und 6 liegen.");

    return daysOfWeek[dayOfWeek];
  }

  private static String monthNameLong(int month) throws Exception
  {
    final String[] months = { "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE",
      "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" };

    if (month < 1 || month > 12)
      throw new Exception("month=" + month + ", muss im Bereich 1 bis 12 liegen.");

    return months[month - 1];
  }

  private static String monthNameShort(int month) throws Exception
  {
    final String[] months = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };

    if (month < 1 || month > 12)
      throw new Exception("month=" + month + ", muss im Bereich 1 bis 12 liegen.");

    return months[month - 1];
  }

  /**
   * Parst das Formatstring und baut den entsprechenden Output zusammen.
   *
   * Es wird ein <tt>StringReader</tt> mit 1 Zeichen Lookahead verwendet,
   * was fuer diese einfache Grammtik vollkommen ausreichend ist.
   *
   * @param format eine Folge von<br>
   * <b>DD</b> Tag des Monats zweistellig (01 - 31)<br>
   * <b>DAY</b>  Wochentagsname (englisch)<br>
   * <b>MM</b>  Monat als Zahl zweistellig (01 - 12)<br>
   * <b>MON</b>  Monat englisch, auf 3 Zeichen abgekürzt<br>
   * <b>MONTH</b>  Monat englisch, ausgeschrieben<br>
   * <b>YY</b>  Jahr zweistellig (die letzten 2 Stellen)<br>
   * <b>YYYY</b>  Jahr vierstellig<br>
   *
   * erlaubt Trennzeichen: <b>/ . _ -</b><br>
   * _ wird auf Space gemappt
   *
   * @throws Exception wenn das Formatstring <tt>format</tt> nicht den Regeln
   * entspricht
   * @throws DateInvalidException wenn das Datum ung&uuml;ltig ist
   * @throws Exception wenn eine <tt>IOException</tt> auftritt
   *
   * @return Das Datum entsprechend formatiert
   */
  private static String formatDate(int year, int month, int day, String format) throws Exception
  {
    StringReader fmt = new StringReader(format);

    final int EOS = -1;

    NumberFormat nf2digits = NumberFormat.getNumberInstance();
    nf2digits.setMinimumIntegerDigits(2);
    nf2digits.setGroupingUsed(false);

    NumberFormat nf4digits = NumberFormat.getNumberInstance();
    nf4digits.setMinimumIntegerDigits(4);
    nf4digits.setGroupingUsed(false);

    StringBuffer ret = new StringBuffer( 40 ); // 40 sollte fuer die meisten format strings als initial laenge,
                                               // gross genug sein

    if (!isDateValid(year, month, day))
      throw new DateInvalidExcpetion(year, month, day);

    Exception formatException = new Exception("Der Aufbau des Formatstrings \"" + format + "\" ist nicht gueltig");

    try
    {
      int la = fmt.read(); // lookahead zeichen, vom Typ in, damit -1 auch codiert werden kann, wäre eigentlich
      // nicht notwendig, da \uFFFF eh nicht im format auftauchen sollte

      while (la != EOS)
      {
        switch ( la )
        {
        case 'D':
        {
          la = fmt.read();
          if (la == 'D')
          {
            // DD
            ret.append( nf2digits.format(day) );

            la = fmt.read();
          } else if (la == 'A')
          {
            la = fmt.read();
            if (la == 'Y')
            {
              // DAY
              ret.append( dayOfWeekName(dayOfWeek(year, month, day)) );

              la = fmt.read();
            } else
              throw formatException;
          } else
            throw formatException;

          break;
        }

        case 'M':
        {
          la = fmt.read();
          if (la == 'M')
          {
            // MM
            ret.append( nf2digits.format(month) );

            la = fmt.read();
          } else if (la == 'O')
          {
            // MON | MONTH
            la = fmt.read();
            if (la == 'N')
            {
              la = fmt.read();
              if (la == 'T')
              {
                la = fmt.read();
                if (la == 'H')
                {
                  // MONTH
                  ret.append( monthNameLong(month) );

                  la = fmt.read();
                } else
                  throw formatException;

              } else
              {
                // MON
                ret.append( monthNameShort(month) );

              }
            } else
              throw formatException;

          } else
            throw formatException;

          break;
        }

        case 'Y':
        {
          la = fmt.read();
          if (la == 'Y')
          {
            // YY | YYYY
            la = fmt.read();
            if (la == 'Y')
            {
              la = fmt.read();
              if (la == 'Y')
              {
                // YYYY
                ret.append( nf4digits.format(year) );

                la = fmt.read();
              } else
                throw formatException;

            } else
            {
              // YY
              ret.append( nf2digits.format(year % 100) );
            }
          } else
            throw formatException;

          break;
        }

        case '/':
        case '.':
        case '-':
          // / . -
          ret.append( (char) la );

          la = fmt.read();
          break;

        case '_':
          // _
          ret.append( ' ' );

          la = fmt.read();
          break;

        default:
          throw formatException;
          // kein break, weil diese zeile eh nicht erreich werden kann

        }
      }
    }
    catch (Exception e)
    {
      throw e;
    }

    return ret.toString();
  }

  /**
   * Hilfsfunktion fuer test_formatDate
   */
  private static boolean test_formatDate_helper(int year, int month, int day, String format, String expectedResult, boolean exceptionExpected)
  {
    boolean bRet = true;
    boolean bException = false;
    try
    {
      String result = formatDate(year, month, day, format);
      bRet &= result.equals(expectedResult);
    }
    catch (Exception e)
    {
      bException = true;
    }

    bRet &= bException == exceptionExpected;

    return bRet;
  }

  /**
   * Testet formatDate
   * @return true, wenn der Test ok war.
   */
  private static boolean test_formatDate(PrintStream log)
  {
    boolean bRet = true;

    bRet &= test_formatDate_helper(1993, 4, 13, "DD.MONTH_YYYY", "13.APRIL 1993", false);
    bRet &= test_formatDate_helper(1989, 10, 20, "DD_MM_YYYY", "20 10 1989", false);
    bRet &= test_formatDate_helper(1976, 1, 2, "DD-MM-YYYY", "02-01-1976", false);
    bRet &= test_formatDate_helper(1976, 1, 2, "DAY_DD/MON.MONTH-MM_YY_YYYY", "FRIDAY 02/JAN.JANUARY-01 76 1976", false);

    // format string error
    bRet &= test_formatDate_helper(1976, 1, 2, " ", "", true);
    bRet &= test_formatDate_helper(1976, 1, 2, "D", "", true);
    bRet &= test_formatDate_helper(1976, 1, 2, "DDD", "", true);
    bRet &= test_formatDate_helper(1976, 1, 2, "DA", "", true);
    bRet &= test_formatDate_helper(1976, 1, 2, "M", "", true);
    bRet &= test_formatDate_helper(1976, 1, 2, "MO", "", true);
    bRet &= test_formatDate_helper(1976, 1, 2, "MONT", "", true);
    bRet &= test_formatDate_helper(1976, 1, 2, "Y", "", true);
    bRet &= test_formatDate_helper(1976, 1, 2, "YYY", "", true);

    log.println((bRet ? "[OK]": "[ERR]") + " test_formatDate");
    return bRet;
  }

  /**
   * Fuehrt das parsen des Datums und das Formatieren des Datums lt. Spez. durch
   * @return das Datum formatiert wie in <tt>format</tt> angegeben oder <tt>"FALSCHE EINGABE"</tt> wenn irgendetwas schief gegangen ist
   */

  private static String maskDate(String date, String format)
  {
    String ret = "";

    try
    {
      if (!(checkFormat("##/##/####", date) || checkFormat("##-##-####", date)))
        throw new Exception("\"" + date + "\" nicht im Format \"##/##/####\" oder \"##-##-####\"");

      int year = new Integer(date.substring(6, 10)).intValue();
      int month = new Integer(date.substring(3, 5)).intValue();
      int day = new Integer(date.substring(0, 2)).intValue();

      if (!isDateValid(year, month, day))
        throw new DateInvalidExcpetion(year, month, day);

      ret = formatDate(year, month, day, format);
    }
    catch (Exception e)
    {
      ret = msgFalscheEingabe;
    }

    return ret;
  }

  /**
   * Testet die Funktion maskDate
   * @return true, wenn der Test ok war.
   */
  private static boolean test_maskDate(PrintStream log)
  {
    boolean bRet = true;

    bRet &= maskDate("13-04-1993", "DD.MONTH_YYYY").equals("13.APRIL 1993");
    bRet &= maskDate("20/10/1989", "DD_MM_YYYY").equals("20 10 1989");
    bRet &= maskDate("29/02.1971", "DD/MON/YY").equals(msgFalscheEingabe);

    log.println((bRet ? "[OK]": "[ERR]") + " test_maskDate");
    return bRet;
  }
}


/**
 * Exception f&uuml;r ein ung&uuml;ltiges Datum.
 * <br><br>
 * Yes I know. Das geh&ouml;hrt eigentlich in eine eigene Datei aber
 * da wir nur ein File abgeben d&uuml;rfen ist es hier.
 * @see maskdate
 */
class DateInvalidExcpetion extends Exception
{
  public DateInvalidExcpetion(int year, int month, int day)
  {
    super("\"" + day + "-" + month + "-" + year + "\" ist kein gueltiges Datum.");
  }
}