/*
 * @(#)UnixParser.java
 *
 * Copyright (c) 2003 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */
package com.jscape.inet.ftp;

import com.jscape.util.IOTools;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Parser responsible for handling FTP directory listing returned in UNIX
 * format. The following is a partial example listing of the format expected.
 * <p/>
 * <p/>
 * <pre>
 * -rw-r--r--    1 vglass   vglass       3651 Feb  2  2002 .screenrc
 * drwxr-xr-x    3 vglass   vglass       4096 Jul 23 17:42 Archive
 * -rw-r--r--    1 vglass   vglass       1096 Jul 23 13:35 GZipTest1.class
 * -rw-r--r--    1 vglass   vglass        733 Jul 23 13:35 GZipTest1.java *
 * </pre>
 */
@SuppressWarnings("unchecked")
public class UnixParser
        implements FtpFileParser {

    private static final String[] MONTHS = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };

    private static final int MONTHS_YEAR = 12;

    private static final int MINIMUM_TOKEN_COUNT = 7;

    // Unix Parser locale
    private Locale currentLocale = Locale.US;

    /**
     * Parses directory listing. Reads directory listing from FTP server and
     * parses contents into an Enumeration of FtpFile.
     *
     * @param in the BufferedReader passed in by Ftp class when invoking
     *           Ftp#getDirListing method.
     *
     * @return an Enumeration of FtpFile
     *
     * @see FtpFile
     * @see Ftp#getDirListing
     */
    public Enumeration parse(BufferedReader in)
            throws IOException {
        Vector files = new Vector();

        String line = in.readLine();

        if (line == null) {
            return files.elements();
        } else if (line.matches(".*Type Doc.*Control.*Sender ID.*Size Date/Time Message ID.*")) {
            LiaisonVANParser liaisonParser = new LiaisonVANParser();
            return liaisonParser.parse(in);
        }
        
        do {
            FtpFile file = parseLine(line);
            if (file != null) {
                files.addElement(file);
            }
        } while ((line = in.readLine()) != null);

        return files.elements();
    }

    /**
     * Gets the date and time for an FtpFile entry sourced from a UNIX directory
     * listing.
     *
     * @param file   the FtpFile
     * @param zone   the TimeZone to use when calculating time. Your default
     *               timezone is used if not provided.
     * @param locale the Locale to use when parsing date. Locale.US is used if not
     *               provided.
     *
     * @return a Date, or null if FtpFile#getDate or FtpFile#getTime values are
     *         null
     *
     * @throws ParseException if an error occurs while parsing entry
     * @see java.util.TimeZone
     * @see java.util.Locale
     * @see FtpFile
     */
    public Date getDateTime(FtpFile file, TimeZone zone, Locale locale)
            throws ParseException {
        Date d = null;
        SimpleDateFormat df = null;

        try {

            // set locale
            if (locale != null) {
                df = new SimpleDateFormat("MMM d HH:mm", locale);
            } else {
                df = new SimpleDateFormat("MMM d HH:mm", Locale.US);
            }

            // set timezone
            if (zone != null) {
                df.setTimeZone(zone);
            }

            d = df.parse(file.getDate() + " " + file.getTime());
            Calendar cal = new GregorianCalendar();
            int currentYear = cal.get(GregorianCalendar.YEAR);
            cal.setTime(d);
            cal.set(GregorianCalendar.YEAR, currentYear);
            d = adjustYear(cal.getTime(), cal, currentYear);

        } catch (ParseException e) {
            if (locale != null) {
                df = new SimpleDateFormat("MMM d yyyy HH:mm", locale);
                d = df.parse(file.getDate() + " " + file.getTime());
            } else {
                df = new SimpleDateFormat("MMM d yyyy HH:mm", Locale.US);
                d = df.parse(file.getDate() + " " + file.getTime());
            }
        }
        return d;
    }

    /**
     * Adjust year.
     *
     * @param date
     * @param currentYear
     *
     * @return modified Date instance.
     */
    private Date adjustYear(Date date, int currentYear) {
        Calendar currentCal = Calendar.getInstance();
        currentCal.setTime(new Date());
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        if (calendar.get(Calendar.MONTH) > currentCal.get(Calendar.MONTH)) {
            calendar.set(GregorianCalendar.YEAR, currentYear - 1);
            date = calendar.getTime();
        }
        return date;
    }

    /**
     * Sets year for FtpFile entries that do not have year information
     *
     * @param date
     * @param calendar
     * @param currentYear
     *
     * @return
     */
    private Date adjustYear(Date date, Calendar calendar, int currentYear) {
        Calendar currentCal = Calendar.getInstance();
        currentCal.setTime(new Date());
        if (date.after(new Date())) {
            calendar.set(GregorianCalendar.YEAR, currentYear - 1);
            date = calendar.getTime();
        }
        return date;
    }

    /**
     * Gets the date and time for an FtpFile entry sourced from a UNIX directory
     * listing.
     *
     * @param file the FtpFile
     *
     * @throws ParseException if an error occurs while parsing entry
     * @see FtpFile
     */
    public Date getDateTime(FtpFile file)
            throws ParseException {
        return getDateTime(file, null, null);
    }

    /**
     * Checks if group is listed in line
     *
     * @param line
     *
     * @return
     */
    private boolean isGroupDisplayed(String line) {
        try {
            StringTokenizer groupCheck = new StringTokenizer(line);
            for (int i = 0; i < 5; ++i) {
                groupCheck.nextToken();
            }
            String month = groupCheck.nextToken().toLowerCase(Locale.ENGLISH);
            return month.length() == 3 && isValidMonth(month);
        } catch (Exception e) {
            return false;
        }

    }

    /**
     * Validates the string argument month is really a month into the locale
     * unix parser
     *
     * @param month The month will be checked
     *
     * @return <code>true</code> if the entry argument is a month into the
     *         locale unix parser; <code>false</code> otherwise
     */
    private boolean isValidMonth(String month) {
        Calendar calendar = Calendar.getInstance();
        String monthName;
        SimpleDateFormat formatter;
        String pattern = "MMM";
        formatter = new SimpleDateFormat(pattern, currentLocale);

        calendar.set(Calendar.DAY_OF_MONTH, 1);

        for (int i = 0; i < MONTHS_YEAR; i++) {
            calendar.set(Calendar.MONTH, i);
            monthName = formatter.format(calendar.getTime());

            if (month.equalsIgnoreCase(monthName)) {
                return true;
            }
        }

        return false;
    }

    private boolean verifyNumberOfTokens(String line) {
        StringTokenizer tokenizer = new StringTokenizer(line, " ", false);
        return tokenizer.countTokens() >= 8;
    }

    public FtpFile parseLine(String line) {
        try {
            if (!verifyNumberOfTokens(line)) {
                return null;
            }

            StringReader reader = new StringReader(line);
            String permissions = readField(reader);
            String id = readField(reader);
            String owner = readField(reader);
            String groupOrSize = readField(reader);
            String sizeOrDate = readField(reader);
            Long possibleSize = parseSize(sizeOrDate);
            String group = possibleSize != null ? groupOrSize : "";
            Long size = possibleSize != null ? possibleSize : parseSize(groupOrSize);
            reader = possibleSize != null ? reader : new StringReader(sizeOrDate + " " + IOTools.readFile(reader));
            UnixParserDate unixDate = readDate(reader);
            String name = readName(reader, isLink(permissions));
            String link = readName(reader, false);


            return new FtpFile(name,
                               size != null ? size : 0L,
                               owner,
                               group,
                               permissions,
                               unixDate.getFormattedDateAsString(),
                               unixDate.getFormattedDateAsString("HH:mm"),
                               isDirectory(permissions),
                               isLink(permissions),
                               link,
                               0,
                               line,
                               unixDate.isYearSet());
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private Long parseSize(String data) {
        try {
            return Long.valueOf(data);
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private char skipWhiteSpace(StringReader reader)
            throws Exception {
        int ch;
        while ((ch = reader.read()) != -1 && Character.isWhitespace((char) ch)) {
            // ok
        }
        return (char) ch;
    }

    private String readField(StringReader reader)
            throws Exception {
        StringBuffer buf = new StringBuffer();
        int ch;
        buf.append(skipWhiteSpace(reader));
        while ((ch = reader.read()) != -1 && !Character.isWhitespace((char) ch)) {
            buf.append((char) ch);
        }
        return buf.toString();
    }

    private long readSize(StringReader reader)
            throws Exception {
        StringBuffer buf = new StringBuffer();
        int ch;
        buf.append(skipWhiteSpace(reader));
        while ((ch = reader.read()) != -1 && !Character.isWhitespace((char) ch)) {
            buf.append((char) ch);
        }

        if (ch == -1) {
            throw new Exception("Unexpected EOF.");
        }

        String num = buf.toString();
        return Long.parseLong(num);
    }

    private String readName(StringReader reader, boolean isLink)
            throws Exception {
        StringBuffer buf = new StringBuffer();
        int ch;
        while ((ch = reader.read()) != -1) {
            buf.append((char) ch);

            if (isLink) {
                String data = buf.toString();
                if (data.endsWith(" -> ")) {
                    return data.substring(0, data.length() - 4);
                }
            }
        }

        return buf.toString();
    }

    /**
     * UnixParserDate Class definition
     *
     * @author mjawwad
     */
    private class UnixParserDate {

        private Date date;
        private String format;
        private boolean yearSet;

        /**
         * Constructor.
         *
         * @param date
         * @param format
         */
        private UnixParserDate(Date date, String format, boolean yearSet) {
            this.date = date;
            this.format = format;
            this.yearSet = yearSet;
        }

        /**
         * Get date.
         *
         * @return date
         */
        public Date getDate() {
            return this.date;
        }

        /**
         * Set date.
         *
         * @param date
         */
        public void setDate(Date date) {
            this.date = date;
        }

        /**
         * Get date format.
         *
         * @return format
         */
        public String getFormat() {
            return this.format;
        }

        /**
         * Set date format
         *
         * @param format
         */
        public void setFormat(String format) {
            this.format = format;
        }

        /**
         * Get if year is specified for the date.
         *
         * @return
         */
        public boolean isYearSet() {
            return this.yearSet;
        }

        /**
         * Get date as a formatted String instance.
         *
         * @return formatted date
         */
        public String getFormattedDateAsString() {
            return new SimpleDateFormat(this.getFormat(), Locale.ENGLISH).format(this.getDate());
        }

        /**
         * Get date as a formatted String instance.
         *
         * @param format
         *
         * @return formatted date
         */
        public String getFormattedDateAsString(String format) {
            return new SimpleDateFormat(format, Locale.ENGLISH).format(this.getDate());
        }
    }

    private UnixParserDate readDate(StringReader reader)
            throws Exception {
        String month = readField(reader);
        String day = readField(reader);
        String yearOrTime = readField(reader);

        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.MONTH, getMonthNo(month));
        calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day));

        int index = yearOrTime.indexOf(':');
        String format = null;
        boolean yearSet = true;
        if (index == -1) {
            calendar.set(Calendar.YEAR, Integer.parseInt(yearOrTime));
            format = "MMM dd yyyy";
        } else {
            calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(yearOrTime.substring(0, index)));
            calendar.set(Calendar.MINUTE, Integer.parseInt(yearOrTime.substring(index + 1)));
            format = "MMM dd";
            yearSet = false;
        }

        return new UnixParserDate(calendar.getTime(), format, yearSet);
    }

    private int getMonthNo(String month)
            throws Exception {
        month = month.toUpperCase(Locale.ENGLISH);

        for (int i = 0; i < MONTHS.length; ++i) {
            if (MONTHS[i].equals(month)) {
                return i;
            }
        }

        throw new Exception("Unsupported month: " + month);
    }

    private boolean isDirectory(String permissions) {
        return permissions.startsWith("d") || permissions.startsWith("l");
    }

    private boolean isLink(String permissions) {
        return permissions.startsWith("l");
    }

    /**
     * Parses a string which contains a date and returns a date object
     *
     * @param date The string will be parsed
     *
     * @return A object date which corresponds with the input string argument
     *
     * @throws ParseException if an error ocurrs
     */
    public Date getFileDate(String date)
            throws ParseException {
        // Entry Format spected
        // Jul 23
        Date fileDate = null;
        try {
            SimpleDateFormat format = new SimpleDateFormat("MMM d", Locale.US);
            fileDate = format.parse(date);
            Calendar fileCalendar = Calendar.getInstance();
            fileCalendar.setTime(fileDate);
            Calendar calendar = Calendar.getInstance();
            int year = calendar.get(Calendar.YEAR);
            fileCalendar.set(Calendar.YEAR, year);
            fileDate = adjustYear(fileCalendar.getTime(), fileCalendar, year);
        } catch (ParseException e) {
            SimpleDateFormat format = new SimpleDateFormat("MMM dd", Locale.US);
            fileDate = format.parse(date);
            Calendar fileCalendar = Calendar.getInstance();
            fileCalendar.setTime(fileDate);
            Calendar calendar = Calendar.getInstance();
            int year = calendar.get(Calendar.YEAR);
            fileCalendar.set(Calendar.YEAR, year);
            fileDate = adjustYear(fileCalendar.getTime(), fileCalendar, year);
        }
        return fileDate;
    }

    /**
     * Sets locale used for parsing date.
     *
     * @param locale the locale
     */
    public void setLocale(Locale locale) {
        this.currentLocale = locale;
    }
}
