/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package com.queplix.core.modules.eqlext.utils;

import com.queplix.core.utils.DateHelper;
import com.queplix.core.utils.StringHelper;
import com.queplix.core.utils.SystemHelper;
import com.queplix.core.utils.log.AbstractLogger;
import com.queplix.core.utils.log.Log;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.TimeZone;


/**
 * Description: Date Parser and processor
 *
 * @author [SVM] Maxim Suponya
 * @author [ALB] Andrey Baranov
 * @version $Revision: 1.1.1.1 $ $Date: 2005/09/12 15:30:41 $
 */

public final class ExtDateParser {

    public static final int MIN_YEAR = 1900;
    public static final int MAX_YEAR = 9999;

    public static final String SECOND_SYMBOL = "s";
    public static final String MINUTE_SYMBOL = "m";
    public static final String HOUR_SYMBOL = "h";
    public static final String DAY_SYMBOL = "d";
    public static final String MONTH_SYMBOL = "M";
    public static final String YEAR_SYMBOL = "y";
    public static final char PLUS_OPERATION = '+';
    public static final char MINUS_OPERATION = '-';
    public static final String TODAY_FUNCTION = "today";
    public static final String NOW_FUNCTION = "now";
    public static final String DECIMAL_SEPARATOR = ".";
    public static final String TIME_SEPARATOR = ":";
    public static final String DATE_TIME_SEPARATOR = " ";

    private static final String[] datePatterns_dayPos0 = {
            "d/M/yy",
            "dd/MM/yy",
            "d/M/yyyy",
            "dd/MM/yyyy",
            "d MMMM yy",
            "dd MMMM yy",
            "d MMMM yyyy",
            "dd MMMM yyyy",
            "ddMMMMyy",
            "ddMMMMyyyy",
            "ddMMMyy",
            "ddMMMyyyy",
            "dd/MMM/yy",
            "dd/MMM/yyyy",
            "ddMMyy",
            "ddMMyyyy"
    };

    private static final String[] datePatterns_dayPos1 = {
            "M/d/yy",
            "MM/dd/yy",
            "M/d/yyyy",
            "MM/dd/yyyy",
            "MMMM d yy",
            "MMMM dd yy",
            "MMMM d yyyy",
            "MMMM dd yyyy",
            "MMMMddyy",
            "MMMMddyyyy",
            "MMMddyy",
            "MMMddyyyy",
            "MMddyy",
            "MMddyyyy"
    };

    private static final String[] datePatterns = new String[datePatterns_dayPos0
            .length + datePatterns_dayPos1.length];

    static {
        System.arraycopy(datePatterns_dayPos0, 0, datePatterns, 0,
                datePatterns_dayPos0.length);
        System.arraycopy(datePatterns_dayPos1, 0, datePatterns,
                datePatterns_dayPos0.length, datePatterns_dayPos1.length);
    }

    private static final String[] timePatterns = {
            "h:mm a",
            "h:mm:ss a",
            "HH:mm:ss",
            "HH:mm"
    };

    private static final AbstractLogger logger = Log.getLog(
            ExtDateParser.class);

    // --------------------------------------------------------------- public static methods

    /**
     * Parses Date from a string. Returns system date millis.
     * Examples of strings to parse:
     * "3/18/2003 +1y" = March 18 2004 00:00:00
     * "today + 1m" = 1 month after today date
     * "now - 5h" = 5 hours befor current time
     * "1jan2003 00:00:00 + 1 10:00" = 1 day and 10 hours after 1 january 2003 00:00:00
     * "today + 0.5y" = half of the year after today
     * "now - 1 .1" = 1 day and 1 minute before current time
     * "January 1 1970 +1y -0.25y +15d -4h -3m -5s +1 1 +1 .1 -5:30 +3:45:01"
     *
     * @param s               - String to parse
     * @param isDayPosFirst   - Position of Day in a Date string, true - date before month, false - month before date
     * @param locale          - user Locale
     * @param tz              - User TimeZone
     * @param dateUserPattern - Date user pattern (example: "MM/dd/yy")
     * @param timeUserPattern - Time user pattern (example: "hh:mm a")
     * @return system long representation of Date
     * @throws ParseException
     */
    public static long parseDate(String s,
                                 boolean isDayPosFirst,
                                 Locale locale,
                                 TimeZone tz,
                                 String dateUserPattern,
                                 String timeUserPattern)
            throws ParseException {

        // checking...
        if(locale == null) {
            throw new IllegalArgumentException("Locale is null");
        }

        if(tz == null) {
            throw new IllegalArgumentException("Timezone is null");
        }

        if(StringHelper.isEmpty(dateUserPattern)) {
            throw new IllegalArgumentException("Date user pattern is null");
        }

        // is null?
        if(StringHelper.isEmpty(s)) {
            throw new ParseException("String date is null", 0);
        }

        int minusPos = s.indexOf(PLUS_OPERATION);
        int plusPos = s.indexOf(MINUS_OPERATION);
        int firstOpPos = -1;
        if(minusPos > 0) {
            firstOpPos = (plusPos < 0) ? minusPos:Math.min(minusPos, plusPos);
        } else if(plusPos > 0) {
            firstOpPos = (minusPos < 0) ? plusPos:Math.min(minusPos, plusPos);

        }
        Date date;

        // Detect date and operation positions
        String dateString;
        String opString;
        if(firstOpPos > 0) {
            dateString = s.substring(0, firstOpPos);
            opString = s.substring(firstOpPos, s.length());
        } else {
            dateString = s;
            opString = null;
        }

        //
        // First. Parse date
        //
        try {
            date = new Date(parseSingleDate(dateString, isDayPosFirst, locale,
                    tz, dateUserPattern, timeUserPattern));
        } catch (ParseException ex) {
            logger.ERROR(ex);

            // set first position
            throw new ParseException(ex.getMessage(), 0);
        }

        if(!StringHelper.isEmpty(opString)) {

            logger.DEBUG("operation: " + opString);

            //
            // Second. Parse operations
            //
            int length = opString.length();
            int i = 0;
            while(i < length) {
                char c = opString.charAt(i++);
                if(c != PLUS_OPERATION && c != MINUS_OPERATION) {
                    continue;
                }

                StringBuffer sb = new StringBuffer();
                while(i < length) {
                    char nextc = opString.charAt(i++);

                    boolean findOp = false;
                    if(i == length) {
                        findOp = true;
                        sb.append(nextc);
                    } else
                    if(nextc == PLUS_OPERATION || nextc == MINUS_OPERATION) {
                        findOp = true;
                        i--;
                    }

                    if(findOp) {
                        // perform math operation
                        try {
                            logger.DEBUG("perform math operation=" + c
                                    + " param=" + sb);

                            long ms = dateMath(date, c, sb.toString());
                            date = new Date(ms);
                        } catch (ParseException ex) {
                            logger.ERROR(ex);

                            // set operation position
                            throw new ParseException(ex.getMessage(),
                                    firstOpPos + i);
                        }
                        break;
                    }

                    sb.append(nextc);
                }
            }
        }

        // check MIN and MAX years
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        int year = cal.get(Calendar.YEAR);
        if(year < MIN_YEAR || year > MAX_YEAR) {
            throw new ParseException("Incorrect year", 0);
        }

        return date.getTime();
    }


    /**
     * Detect has date a time part
     *
     * @param s given date
     * @return true if has
     */
    public static boolean hasTimePart(String s) {
        if(s == null) {
            return false;
        } else if(s.toLowerCase().indexOf(NOW_FUNCTION) >= 0) {
            return true;
        } else {
            return (s.indexOf(TIME_SEPARATOR) >= 0);
        }
    }

    // --------------------------------------------------------------- private static methods

    /**
     * Helper method to parse a date from a string
     *
     * @param s               - String to parse
     * @param isDayPosFirst   - Position of Day in a Date string, true - date before month, false - month before date
     * @param locale          - user Locale
     * @param tz              - User TimeZone
     * @param dateUserPattern - Date user pattern (example: "MM/dd/yy")
     * @param timeUserPattern - Time user pattern (example: "hh:mm a")
     * @return milleseconds
     * @throws ParseException
     */
    private static long parseSingleDate(String s,
                                        boolean isDayPosFirst,
                                        Locale locale,
                                        TimeZone tz,
                                        String dateUserPattern,
                                        String timeUserPattern)
            throws ParseException {

        // Initialization
        ParseException parseException = null;
        String[] datePatterns = null;
        if(isDayPosFirst) {
            datePatterns = datePatterns_dayPos0;
        } else {
            datePatterns = datePatterns_dayPos1;

        }
        s = s.trim();
        boolean hasTimePart = (s.indexOf(':') >= 0);

        // now?
        if(s.equalsIgnoreCase(NOW_FUNCTION)) {
            return DateHelper.currentTimeMillis();
        }

        // today?
        if(s.equalsIgnoreCase(TODAY_FUNCTION)) {
            Calendar cal = Calendar.getInstance();
            cal.setTime(new Date(DateHelper.toUser(
                    DateHelper.currentTimeMillis(), tz)));
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);

            return DateHelper.toSystem(cal.getTime().getTime(), tz);
        }

        //
        // try user pattern
        //
        try {
            String userPattern = dateUserPattern;
            if(hasTimePart && timeUserPattern != null) {
                userPattern += DATE_TIME_SEPARATOR + timeUserPattern;
            }
            SimpleDateFormat sdf = new SimpleDateFormat(userPattern, locale);
            return DateHelper.toSystem(sdf.parse(s).getTime(), tz);

        } catch (ParseException e) {
            parseException = e;
        }

        //
        // try other possible patterns
        //
        if(hasTimePart) {
            // try to parse date WITH time
            for(int i = 0; i < datePatterns.length; i++) {
                for(int j = 0; j < timePatterns.length; j++) {
                    try {
                        return tryToParse(s, datePatterns[i]
                                + DATE_TIME_SEPARATOR + timePatterns[j], locale,
                                tz);
                    } catch (ParseException e) {
                    }
                }
            }

        } else {
            // try to parse date WITHOUT time
            for(int i = 0; i < datePatterns.length; i++) {
                try {
                    return tryToParse(s, datePatterns[i], locale, tz);
                } catch (ParseException e) {
                }
            }
        }

        // throw exception
        throw parseException;
    }


    /**
     * Helper Method to try to parse a date
     *
     * @param s       - String to parse
     * @param pattern - Pattern
     * @param locale  - User Locale
     * @param tz      - User TimeZone
     * @return long representation of date
     * @throws ParseException
     */
    private static long tryToParse(String s, String pattern, Locale locale,
                                   TimeZone tz)
            throws ParseException {

        // maybe user locale?
        try {
            SimpleDateFormat sdf = new SimpleDateFormat(pattern, locale);
            return DateHelper.toSystem(sdf.parse(s).getTime(), tz);

        } catch (ParseException ex1) {
            // maybe system locale?
            try {
                SimpleDateFormat sdf = new SimpleDateFormat(pattern,
                        SystemHelper.SYSTEM_LOCALE);
                return DateHelper.toSystem(sdf.parse(s).getTime(), tz);
            } catch (ParseException ex2) {
                // throw top exception
                throw ex1;
            }
        }
    }


    /**
     * Changes date according to operation and an argumet stinrg s
     * Example: "January 1 2003 0:00:00", "+", "1h" will return January 1 2003 1:00:00
     *
     * @param date      - Date to work on
     * @param operation - math operation, currently can be a PLUS_OPERATION or a MINUS_OPERATION
     * @param s         - an argument
     * @return long reprsentation of date
     * @throws ParseException
     */
    private static long dateMath(Date date, char operation, String s)
            throws ParseException {

        int op;
        if(operation == MINUS_OPERATION) {
            op = -1;
        } else {
            op = 1;

        }
        s = s.trim();

        DateMathHelper dmh = new DateMathHelper();
        if(s.indexOf(DATE_TIME_SEPARATOR) == -1 && s.indexOf(TIME_SEPARATOR)
                == -1) {
            dmh.chekForLetterIdentifers(s);
            dmh.proceedRemainder();

        } else {

            StringTokenizer st = new StringTokenizer(s, TIME_SEPARATOR, false);
            List params = new ArrayList();
            while(st.hasMoreTokens()) {
                params.add(st.nextToken());

            }
            String firstParam = (String) params.get(0);
            int pos1 = firstParam.indexOf(DATE_TIME_SEPARATOR);
            if(pos1 > 0) {
                String firstPart = firstParam.substring(0, pos1);
                dmh.setDays(parseDouble(firstPart));
                dmh.proceedRemainder();

                // if it happen to be something like .1 or 1.
                String secondPart = firstParam.substring(pos1 + 1);
                int pos2 = secondPart.indexOf(DECIMAL_SEPARATOR);
                if(pos2 == 0) {
                    dmh.setMinutes(parseDouble(secondPart.substring(1)));
                } else if(pos2 == secondPart.length()) {
                    dmh.setHours(parseDouble(secondPart.substring(0,
                            pos2 - 1)));
                } else {
                    // if not, then it's probably hours..
                    dmh.setHours(parseDouble(secondPart));
                }

            } else {
                dmh.setHours(parseDouble(firstParam));
            }

            if(params.size() == 2) {
                dmh.setMinutes(parseDouble((String) params.get(1)));
            } else if(params.size() == 3) {
                dmh.setMinutes(parseDouble((String) params.get(1)));
                dmh.setSeconds(parseDouble((String) params.get(2)));
            }
        }

        return dmh.getDate(date, op);
    }


    // Try to parse string as double
    // And throw ParseException if failed
    private static double parseDouble(String s)
            throws ParseException {

        try {
            return Double.parseDouble(s);

        } catch (NumberFormatException ex) {
            throw new ParseException(ex.getMessage(), 0);
        }
    }

    //------------------------------------------------------------ data processors

    /**
     * Helper inner class for dateMath method
     */
    private static class DateMathHelper {
        private double years = 0;
        private double months = 0;
        private double days = 0;
        private double hours = 0;
        private double minutes = 0;
        private double seconds = 0;


        //check if there's something familiar
        public void chekForLetterIdentifers(String s)
                throws ParseException {

            int pos;
            if((pos = s.indexOf(YEAR_SYMBOL)) > 0) {
                String numberOfYears = s.substring(0, pos);
                years = parseDouble(numberOfYears);

            } else if((pos = s.indexOf(MONTH_SYMBOL)) > 0) {
                String numberOfMonths = s.substring(0, pos);
                months = parseDouble(numberOfMonths);

            } else if((pos = s.indexOf(DAY_SYMBOL)) > 0) {
                String numberOfDays = s.substring(0, pos);
                days = parseDouble(numberOfDays);

            } else if((pos = s.indexOf(HOUR_SYMBOL)) > 0) {
                String numberOfHours = s.substring(0, pos);
                hours = parseDouble(numberOfHours);

            } else if((pos = s.indexOf(MINUTE_SYMBOL)) > 0) {
                String numberOfMinutes = s.substring(0, pos);
                minutes = parseDouble(numberOfMinutes);

            } else if((pos = s.indexOf(SECOND_SYMBOL)) > 0) {
                String numberOfSeconds = s.substring(0, pos);
                seconds = parseDouble(numberOfSeconds);

            } else {
                hours = parseDouble(s);
            }
        }


        //if it wasn't an integer, then move reminder down through months, days, hours...
        public void proceedRemainder() {

            double i;

            if(years < 1 && years > 0) {
                months = 12 * years;
            } else if((i = years % (int) years) > 0) {
                months = 12 * i;

            }
            if(months < 1 && months > 0) {
                days = 30 * months;
            } else if((i = months % (int) months) > 0) {
                days = 30 * i;

            }
            if(days < 1 && days > 0) {
                hours = 24 * days;
            } else if((i = days % (int) days) > 0) {
                hours = 24 * i;

            }
            if(hours < 1 && hours > 0) {
                minutes = 60 * hours;
            } else if((i = hours % (int) hours) > 0) {
                minutes = 60 * i;

            }
            if(minutes < 1 && minutes > 0) {
                seconds = 60 * minutes;
            } else if((i = minutes % (int) minutes) > 0) {
                seconds = 60 * i;
            }
        }

        public long getDate(Date date, int op) {
            Calendar cal = Calendar.getInstance();
            cal.setTime(date);

            if(years != 0) {
                cal.add(Calendar.YEAR, (int) years * op);

            }
            if(months != 0) {
                cal.add(Calendar.MONTH, (int) months * op);

            }
            if(days != 0) {
                cal.add(Calendar.DATE, (int) days * op);

            }
            if(hours != 0) {
                cal.add(Calendar.HOUR, (int) hours * op);

            }
            if(minutes != 0) {
                cal.add(Calendar.MINUTE, (int) minutes * op);

            }
            if(seconds != 0) {
                cal.add(Calendar.SECOND, (int) seconds * op);

            }
            return cal.getTime().getTime();
        }

        public double getYears() {
            return years;
        }

        public void setYears(double years) {
            this.years = years;
        }

        public double getMonths() {
            return months;
        }

        public void setMonths(double months) {
            this.months = months;
        }

        public double getDays() {
            return days;
        }

        public void setDays(double days) {
            this.days = days;
        }

        public double getHours() {
            return hours;
        }

        public void setHours(double hours) {
            this.hours = hours;
        }

        public double getMinutes() {
            return minutes;
        }

        public void setMinutes(double minutes) {
            this.minutes = minutes;
        }

        public double getSeconds() {
            return seconds;
        }

        public void setSeconds(double seconds) {
            this.seconds = seconds;
        }
    }
}
