/*
 * Copyright 2004-2018 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (http://h2database.com/html/license.html).
 * Initial Developer: Daniel Gredler
 */
package org.h2.util;

import static java.lang.String.format;

import java.sql.Timestamp;
import java.util.Calendar;
import java.util.List;

/**
 * Emulates Oracle's TO_DATE function.<br>
 * This class holds and handles the input data form the TO_DATE-method
 */
public class ToDateParser {
    private final String unmodifiedInputStr;
    private final String unmodifiedFormatStr;
    private final ConfigParam functionName;
    private String inputStr;
    private String formatStr;
    private final Calendar resultCalendar = DateTimeUtils.createGregorianCalendar();
    private Integer nanos;

    /**
     * @param input the input date with the date-time info
     * @param format the format of date-time info
     * @param functionName one of [TO_DATE, TO_TIMESTAMP] (both share the same
     *            code)
     */
    private ToDateParser(ConfigParam functionName, String input, String format) {
        // reset calendar - default oracle behaviour
        resultCalendar.set(Calendar.YEAR, 1970);
        resultCalendar.set(Calendar.MONTH, DateTimeUtils.createGregorianCalendar().get(Calendar.MONTH));
        resultCalendar.clear(Calendar.DAY_OF_YEAR);
        resultCalendar.clear(Calendar.DAY_OF_WEEK);
        resultCalendar.clear(Calendar.DAY_OF_WEEK_IN_MONTH);
        resultCalendar.set(Calendar.DAY_OF_MONTH, 1);
        resultCalendar.set(Calendar.HOUR, 0);
        resultCalendar.set(Calendar.HOUR_OF_DAY, 0);
        resultCalendar.set(Calendar.MINUTE, 0);
        resultCalendar.set(Calendar.SECOND, 0);
        resultCalendar.set(Calendar.MILLISECOND, 0);
        resultCalendar.set(Calendar.AM_PM, Calendar.AM);

        this.functionName = functionName;
        inputStr = input.trim();
        // Keep a copy
        unmodifiedInputStr = inputStr;
        if (format == null || format.isEmpty()) {
            // default Oracle format.
            formatStr = functionName.getDefaultFormatStr();
        } else {
            formatStr = format.trim();
        }
        // Keep a copy
        unmodifiedFormatStr = formatStr;
    }

    private static ToDateParser getDateParser(String input, String format) {
        ToDateParser result = new ToDateParser(ConfigParam.TO_DATE, input, format);
        parse(result);
        return result;
    }

    private static ToDateParser getTimestampParser(String input, String format) {
        ToDateParser result = new ToDateParser(ConfigParam.TO_TIMESTAMP, input, format);
        parse(result);
        return result;
    }

    private Timestamp getResultingTimestamp() {
        Calendar cal = (Calendar) getResultCalendar().clone();
        int nanosToSet = nanos == null ?
                cal.get(Calendar.MILLISECOND) * 1000000 : nanos.intValue();
        cal.set(Calendar.MILLISECOND, 0);
        Timestamp ts = new Timestamp(cal.getTimeInMillis());
        ts.setNanos(nanosToSet);
        return ts;
    }

    Calendar getResultCalendar() {
        return resultCalendar;
    }

    String getInputStr() {
        return inputStr;
    }

    String getFormatStr() {
        return formatStr;
    }

    String getFunctionName() {
        return functionName.name();
    }

    void setNanos(int nanos) {
        this.nanos = nanos;
    }

    private boolean hasToParseData() {
        return formatStr.length() > 0;
    }

    private void removeFirstChar() {
        if (!formatStr.isEmpty()) {
            formatStr = formatStr.substring(1);
        }
        if (!inputStr.isEmpty()) {
            inputStr = inputStr.substring(1);
        }
    }

    private static ToDateParser parse(ToDateParser p) {
        while (p.hasToParseData()) {
            List<ToDateTokenizer.FormatTokenEnum> tokenList =
                    ToDateTokenizer.FormatTokenEnum.getTokensInQuestion(p.getFormatStr());
            if (tokenList.isEmpty()) {
                p.removeFirstChar();
                continue;
            }
            boolean foundAnToken = false;
            for (ToDateTokenizer.FormatTokenEnum token : tokenList) {
                if (token.parseFormatStrWithToken(p)) {
                    foundAnToken = true;
                    break;
                }
            }
            if (!foundAnToken) {
                p.removeFirstChar();
                continue;
            }
        }
        return p;
    }

    /**
     * Remove a token from a string.
     *
     * @param inputFragmentStr the input fragment
     * @param formatFragment the format fragment
     */
    void remove(String inputFragmentStr, String formatFragment) {
        if (inputFragmentStr != null && inputStr.length() >= inputFragmentStr.length()) {
            inputStr = inputStr.substring(inputFragmentStr.length());
        }
        if (formatFragment != null && formatStr.length() >= formatFragment.length()) {
            formatStr = formatStr.substring(formatFragment.length());
        }
    }

    @Override
    public String toString() {
        int inputStrLen = inputStr.length();
        int orgInputLen = unmodifiedInputStr.length();
        int currentInputPos = orgInputLen - inputStrLen;
        int restInputLen = inputStrLen <= 0 ? inputStrLen : inputStrLen - 1;

        int orgFormatLen = unmodifiedFormatStr.length();
        int currentFormatPos = orgFormatLen - formatStr.length();

        StringBuilder sb = new StringBuilder();
        sb.append(format("\n    %s('%s', '%s')", functionName,
                unmodifiedInputStr, unmodifiedFormatStr));
        sb.append(format("\n      %s^%s ,  %s^ <-- Parsing failed at this point",
                format("%" + (functionName.name().length() + currentInputPos) + "s", ""),
                restInputLen <= 0 ? "" : format("%" + restInputLen + "s", ""),
                currentFormatPos <= 0 ? "" : format("%" + currentFormatPos + "s", "")));

        return sb.toString();
    }

    /**
     * Parse a string as a timestamp with the given format.
     *
     * @param input the input
     * @param format the format
     * @return the timestamp
     */
    public static Timestamp toTimestamp(String input, String format) {
        ToDateParser parser = getTimestampParser(input, format);
        return parser.getResultingTimestamp();
    }

    /**
     * Parse a string as a date with the given format.
     *
     * @param input the input
     * @param format the format
     * @return the date as a timestamp
     */
    public static Timestamp toDate(String input, String format) {
        ToDateParser parser = getDateParser(input, format);
        return parser.getResultingTimestamp();
    }

    /**
     * The configuration of the date parser.
     */
    private enum ConfigParam {
        TO_DATE("DD MON YYYY"),
        TO_TIMESTAMP("DD MON YYYY HH:MI:SS");

        private final String defaultFormatStr;
        ConfigParam(String defaultFormatStr) {
            this.defaultFormatStr = defaultFormatStr;
        }
        String getDefaultFormatStr() {
            return defaultFormatStr;
        }

    }

}