package org.xbib.files.ftp.parser;

import org.xbib.files.ftp.FTPFile;
import org.xbib.files.ftp.FTPFileEntryParserImpl;

import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.HashMap;
import java.util.Locale;
import java.util.Objects;

/**
 * Parser class for MSLT and MLSD replies. See RFC 3659.
 * <p>
 * Format is as follows:
 * <pre>
 * entry            = [ facts ] SP pathname
 * facts            = 1*( fact ";" )
 * fact             = factname "=" value
 * factname         = "Size" / "Modify" / "Create" /
 *                    "Type" / "Unique" / "Perm" /
 *                    "Lang" / "Media-Type" / "CharSet" /
 * os-depend-fact / local-fact
 * os-depend-fact   = {IANA assigned OS name} "." token
 * local-fact       = "X." token
 * value            = *SCHAR
 *
 * Sample os-depend-fact:
 * UNIX.group=0;UNIX.mode=0755;UNIX.owner=0;
 * </pre>
 * A single control response entry (MLST) is returned with a leading space;
 * multiple (data) entries are returned without any leading spaces.
 * The parser requires that the leading space from the MLST entry is removed.
 * MLSD entries can begin with a single space if there are no facts.
 *
 */
public class MLSxEntryParser extends FTPFileEntryParserImpl {
    // This class is immutable, so a single instance can be shared.
    private static final MLSxEntryParser PARSER = new MLSxEntryParser();

    private static final HashMap<String, Integer> TYPE_TO_INT = new HashMap<>();
    private static final int[] UNIX_GROUPS = { // Groups in order of mode digits
            FTPFile.USER_ACCESS,
            FTPFile.GROUP_ACCESS,
            FTPFile.WORLD_ACCESS,
    };
    private static final int[][] UNIX_PERMS = { // perm bits, broken down by octal int value
/* 0 */  {},
/* 1 */  {FTPFile.EXECUTE_PERMISSION},
/* 2 */  {FTPFile.WRITE_PERMISSION},
/* 3 */  {FTPFile.EXECUTE_PERMISSION, FTPFile.WRITE_PERMISSION},
/* 4 */  {FTPFile.READ_PERMISSION},
/* 5 */  {FTPFile.READ_PERMISSION, FTPFile.EXECUTE_PERMISSION},
/* 6 */  {FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION},
/* 7 */  {FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION, FTPFile.EXECUTE_PERMISSION},
    };

    static {
        TYPE_TO_INT.put("file", FTPFile.FILE_TYPE);
        TYPE_TO_INT.put("cdir", FTPFile.DIRECTORY_TYPE); // listed directory
        TYPE_TO_INT.put("pdir", FTPFile.DIRECTORY_TYPE); // a parent dir
        TYPE_TO_INT.put("dir", FTPFile.DIRECTORY_TYPE); // dir or sub-dir
    }

    /**
     * Create the parser for MSLT and MSLD listing entries
     * This class is immutable, so one can use {@link #getInstance()} instead.
     */
    public MLSxEntryParser() {
        super();
    }

    /**
     * Parse a GMT time stamp of the form YYYYMMDDHHMMSS[.sss]
     *
     * @param timestamp the date-time to parse
     * @return a zoned date time, may be {@code null}
     */
    public static ZonedDateTime parseGMTdateTime(String timestamp) {
        try {
            DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
                    .appendPattern( "yyyyMMddHHmmss.SSS")
                    .toFormatter()
                    .withZone(ZoneId.of("UTC"))
                    .withLocale(Locale.ROOT);
            // adjust future dates to one year ago
            ZonedDateTime zonedDateTime = ZonedDateTime.parse(timestamp, dateTimeFormatter);
            if (zonedDateTime.isAfter(ZonedDateTime.now())) {
                zonedDateTime = zonedDateTime.minusYears(1L);
            }
            return zonedDateTime;
        } catch (DateTimeParseException e) {
            //
        }
        try {
            DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
                    .appendPattern( "yyyyMMddHHmmss")
                    .parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
                    .toFormatter()
                    .withZone(ZoneId.of("UTC"))
                    .withLocale(Locale.ROOT);
            // adjust future dates to one year ago
            ZonedDateTime zonedDateTime = ZonedDateTime.parse(timestamp, dateTimeFormatter);
            if (zonedDateTime.isAfter(ZonedDateTime.now())) {
                zonedDateTime = zonedDateTime.minusYears(1L);
            }
            return zonedDateTime;
        } catch (DateTimeParseException e) {
            //
        }
        String[] datePatterns = {
                "yyyyMMdd",
                "yyyy"
        };
        for (String pattern : datePatterns) {
            try {
                DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
                        .appendPattern(pattern)
                        .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
                        .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
                        .toFormatter();
                LocalDate localDate = LocalDate.parse(timestamp, dateTimeFormatter);
                ZonedDateTime zonedDateTime = localDate.atTime(0, 0, 0).atZone(ZoneId.of("UTC"));
                // adjust future dates to one year ago
                if (zonedDateTime.isAfter(ZonedDateTime.now())) {
                    zonedDateTime = zonedDateTime.minusYears(1L);
                }
                return zonedDateTime;
            } catch (DateTimeParseException e) {
                //
            }
        }
        return null;
    }

    public static FTPFile parseEntry(String entry) {
        return PARSER.parseFTPEntry(entry);
    }

    public static MLSxEntryParser getInstance() {
        return PARSER;
    }

    @Override
    public FTPFile parseFTPEntry(String entry) {
        if (entry.startsWith(" ")) {// leading space means no facts are present
            if (entry.length() > 1) { // is there a path name?
                FTPFile file = new FTPFile();
                file.setRawListing(entry);
                file.setName(entry.substring(1));
                return file;
            } else {
                return null; // Invalid - no pathname
            }
        }
        String[] parts = entry.split(" ", 2); // Path may contain space
        if (parts.length != 2 || parts[1].length() == 0) {
            return null; // no space found or no file name
        }
        final String factList = parts[0];
        if (!factList.endsWith(";")) {
            return null;
        }
        FTPFile file = new FTPFile();
        file.setRawListing(entry);
        file.setName(parts[1]);
        String[] facts = factList.split(";");
        boolean hasUnixMode = parts[0].toLowerCase(Locale.ENGLISH).contains("unix.mode=");
        for (String fact : facts) {
            String[] factparts = fact.split("=", -1); // Don't drop empty values
// Sample missing permission
// drwx------   2 mirror   mirror       4096 Mar 13  2010 subversion
// modify=20100313224553;perm=;type=dir;unique=811U282598;UNIX.group=500;UNIX.mode=0700;UNIX.owner=500; subversion
            if (factparts.length != 2) {
                return null; // invalid - there was no "=" sign
            }
            String factname = factparts[0].toLowerCase(Locale.ENGLISH);
            String factvalue = factparts[1];
            if (factvalue.length() == 0) {
                continue; // nothing to see here
            }
            String valueLowerCase = factvalue.toLowerCase(Locale.ENGLISH);
            if ("size".equals(factname)) {
                file.setSize(Long.parseLong(factvalue));
            } else if ("sizd".equals(factname)) { // Directory size
                file.setSize(Long.parseLong(factvalue));
            } else if ("modify".equals(factname)) {
                ZonedDateTime parsed = parseGMTdateTime(factvalue);
                if (parsed == null) {
                    return null;
                }
                file.setTimestamp(parsed);
            } else if ("type".equals(factname)) {
                Integer intType = TYPE_TO_INT.get(valueLowerCase);
                file.setType(Objects.requireNonNullElse(intType, FTPFile.UNKNOWN_TYPE));
            } else if (factname.startsWith("unix.")) {
                String unixfact = factname.substring("unix.".length()).toLowerCase(Locale.ENGLISH);
                switch (unixfact) {
                    case "group":
                        file.setGroup(factvalue);
                        break;
                    case "owner":
                        file.setUser(factvalue);
                        break;
                    case "mode":  // e.g. 0[1]755
                        int off = factvalue.length() - 3; // only parse last 3 digits
                        for (int i = 0; i < 3; i++) {
                            int ch = factvalue.charAt(off + i) - '0';
                            if (ch >= 0 && ch <= 7) { // Check it's valid octal
                                for (int p : UNIX_PERMS[ch]) {
                                    file.setPermission(UNIX_GROUPS[i], p, true);
                                }
                            }
                        }
                        break;
                }
            }
            else if (!hasUnixMode && "perm".equals(factname)) { // skip if we have the UNIX.mode
                doUnixPerms(file, valueLowerCase);
            }
        }
        return file;
    }

    //              perm-fact    = "Perm" "=" *pvals
    //              pvals        = "a" / "c" / "d" / "e" / "f" /
    //                             "l" / "m" / "p" / "r" / "w"
    private void doUnixPerms(FTPFile file, String valueLowerCase) {
        for (char c : valueLowerCase.toCharArray()) {
            // TODO these are mostly just guesses at present
            switch (c) {
                case 'a':     // (file) may APPEnd
                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
                    break;
                case 'c':     // (dir) files may be created in the dir
                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
                    break;
                case 'd':     // deletable
                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
                    break;
                case 'e':     // (dir) can change to this dir
                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
                    break;
                case 'f':     // (file) renamable
                    // ?? file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
                    break;
                case 'l':     // (dir) can be listed
                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, true);
                    break;
                case 'm':     // (dir) can create directory here
                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
                    break;
                case 'p':     // (dir) entries may be deleted
                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
                    break;
                case 'r':     // (files) file may be RETRieved
                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
                    break;
                case 'w':     // (files) file may be STORed
                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
                    break;
                default:
                    break;
            }
        }
    }
}
