/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.apache.ftpserver.filesystem.db;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;
import javax.sql.DataSource;
import org.apache.ftpserver.ftplet.FtpFile;

/**
 *
 * @author brett
 */
public class DbFile implements FtpFile {

    public DbFile(String path, String name, DataSource dataSource) {
        this.path = path;
        this.name = name;
        this.dataSource = dataSource;
    }

    public DbFile(String absolutePath, DataSource dataSource) {
        if (absolutePath.endsWith("/")) {
            absolutePath = absolutePath.substring(0, absolutePath.length() - 1);
        }
        this.path = getDirectoryFromPath(absolutePath);
        this.name = getNameFromPath(absolutePath);
        this.dataSource = dataSource;
    }

    /**
     * Value of DataSource
     */
    private DataSource dataSource;

    /**
     * Value of path
     */
    private String path;

    /**
     * Value of the file name
     */
    private String name;

    /**
     * The Id of this library resource
     */
    private Integer resourceId;

    /**
     * Value of directory
     */
    private boolean directory;

    /**
     * Value of readonly
     */
    private boolean readOnly;

    /**
     * The resource size
     */
    private Integer resourceSize;

    /**
     * The resource date
     */
    private Date resourceDate;

    public String getAbsolutePath() {
        if ("".equals(path) && "/".equals(name)) {
            return name;
        } else {
            if (path.endsWith("/")) {
                return path + name;
            } else {
                return path + "/" + name;
            }
        }
    }

    public String getName() {
        return name;
    }

    public boolean isHidden() {
        return false;
    }

    public boolean isDirectory() {
        updateFromDatabase();
        return this.directory;
    }

    public boolean isFile() {
        updateFromDatabase();
        return !this.directory;
    }

    public boolean doesExist() {
        boolean res = false;

        updateFromDatabase();
        if (null != this.resourceId) {
            res = true;
        }

        return res;
    }

    public boolean isReadable() {
        boolean res = false;

        updateFromDatabase();
        if (null != this.resourceId && !this.directory) {
            res = true;
        }

        res = true;
        return res;
    }

    public boolean isWritable() {
        boolean res = false;

        updateFromDatabase();
        if (this.directory || !this.readOnly) {
            res = true;
        }

        return res;
    }

    public boolean isRemovable() {
        boolean res = false;
        ResultSet rs = null;
        Statement stmt = null;

        updateFromDatabase();
        if (null != this.resourceId && !this.readOnly) {
            if (isDirectory()) {
                try {
                    stmt = createConnection().createStatement();
                    rs = stmt.executeQuery("SELECT COUNT(*) AS N FROM LIBRARY_RESOURCE R " +
                            "WHERE R.PARENT_ID = " + resourceId);
                    if (rs.next()) {
                        int n = rs.getInt("n");
                        if (n == 0) {
                            res = true;
                        }
                    }
                } catch (SQLException sQLException) {
                } finally {
                    closeQuitely(rs);
                    closeQuitely(stmt);
                }
            } else {
                res = true;
            }
        }

        return res;
    }

    public String getOwnerName() {
        return "root";
    }

    public String getGroupName() {
        return "other";
    }

    public int getLinkCount() {
        return 1;
    }

    public long getLastModified() {
        long res = 0;

        updateFromDatabase();
        if (null != this.resourceId) {
            res = this.resourceDate.getTime();
        }

        return res;
    }

    public boolean setLastModified(long arg0) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public long getSize() {
        long res = 0;

        updateFromDatabase();
        if (null != this.resourceId) {
            res = this.resourceSize;
        }

        return res;
    }

    public boolean mkdir() {
        boolean res = false;
        String parentPath;
        String parentName;
        ResultSet rs = null;
        Statement stmt = null;

        if (isWritable()) {
            try {
                parentPath = getDirectoryFromPath(path);
                parentName = getNameFromPath(path);
                stmt = createConnection().createStatement();
                rs = stmt.executeQuery("SELECT R.ID FROM LIBRARY_RESOURCE R " +
                        "WHERE R.PATH = '" + parentPath + "' AND R.NAME = '" + parentName + "'");
                if (rs.next()) {
                    int id = rs.getInt("id");
                    stmt.executeUpdate("INSERT INTO LIBRARY_RESOURCE (PARENT_ID, PATH, NAME, IS_FOLDER, IS_READONLY, RESOURCE_DATE, RESOURCE_SIZE) " +
                            "VALUES (" + id + ", '" + path + "', '" + name + "', 1, 0, CURRENT_TIMESTAMP, 0)");
                    res = true;
                }
            } catch (SQLException sQLException) {
                sQLException.printStackTrace();
            } finally {
                closeQuitely(stmt);
            }
        }

        return res;
    }

    public boolean delete() {
        boolean res = false;

        Statement stmt = null;
        ResultSet rs = null;
        try {
            stmt = createConnection().createStatement();
            rs = stmt.executeQuery("SELECT R.ID FROM LIBRARY_RESOURCE R " +
                    "WHERE R.PATH = '" + path + "' AND R.NAME = '" + name + "'");
            if (rs.next()) {
                int id = rs.getInt("id");
                stmt.executeUpdate("DELETE FROM LIBRARY_RESOURCE R " +
                        "WHERE " +
                        "R.ID = " + id);
            }
            res = true;
        } catch (SQLException sQLException) {
        } finally {
            closeQuitely(stmt);
            closeQuitely(rs);
        }

        return res;
    }

    public boolean move(FtpFile arg0) {
        boolean res = false;

        return res;
    }

    public List<FtpFile> listFiles() {
        List<FtpFile> res = new ArrayList<FtpFile>();

        updateFromDatabase();
        if (null != this.resourceId && this.directory) {
            Statement stmt = null;
            ResultSet rs = null;
            try {
                // execute query
                stmt = createConnection().createStatement();
                rs = stmt.executeQuery("SELECT R.PATH, R.NAME " +
                        "from " +
                        "  LIBRARY_RESOURCE R " +
                        "  WHERE " +
                        "  R.PARENT_ID = " + this.resourceId);
                while (rs.next()) {
                    String fullPath = rs.getString("PATH") + "/" + rs.getString("NAME");
                    DbFile dbFile = new DbFile(fullPath, this.dataSource);
                    res.add(dbFile);
                }
            } catch (SQLException x) {
            } finally {
                closeQuitely(stmt);
                closeQuitely(rs);
            }
        }

        return res;
    }

    public OutputStream createOutputStream(long arg0) throws IOException {
        if (!isWritable()) {
            throw new IOException("No write permissiong: " + this.name);
        }
        return new DbFileOutputStream();
    }

    public InputStream createInputStream(long offset) throws IOException {
        if (!isReadable()) {
            throw new IOException("No read permission : " + this.name);
        }
        return new DbFileInputStream();
    }

    /**
     * Open connection to database.
     */
    protected Connection createConnection() throws SQLException {
        Connection connection = this.dataSource.getConnection();
        connection.setAutoCommit(true);

        return connection;
    }

    private void closeQuitely(Statement stmt) {
        if (stmt != null) {
            Connection con = null;
            try {
                con = stmt.getConnection();
            } catch (Exception e) {
            }
            try {
                stmt.close();
            } catch (SQLException e) {
                // ignore
            }
            closeQuitely(con);
        }
    }

    private void closeQuitely(ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                // ignore
            }
        }
    }

    protected void closeQuitely(Connection con) {
        if (con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                // ignore
            }
        }
    }

    private String getDirectoryFromPath(String path) {
        String dir = "";

        if ("/".equals(path)) {
            dir = "";
        } else {
            int lastSlashIndex = path.lastIndexOf('/');
            if (-1 != lastSlashIndex) {
                if (0 == lastSlashIndex) {
                    dir = "/";
                } else {
                    dir = path.substring(0, lastSlashIndex);
                }
            }
        }

        return dir;
    }

    private String getNameFromPath(String path) {
        String name = "";

        if ("/".equals(path)) {
            name = "/";
        } else {
            int lastSlashIndex = path.lastIndexOf('/');
            if (-1 != lastSlashIndex) {
                name = path.substring(lastSlashIndex + 1);
            }
        }
        return name;
    }

    protected void updateFromDatabase() {

        Statement stmt = null;
        ResultSet rs = null;
        try {
            // execute query
            stmt = createConnection().createStatement();
            rs = stmt.executeQuery("SELECT R.ID, R.IS_FOLDER, R.RESOURCE_DATE, R.RESOURCE_SIZE FROM LIBRARY_RESOURCE R " +
                    "WHERE R.PATH = '" + path + "' AND R.NAME = '" + name + "'");
            if (rs.next()) {
                this.resourceId = rs.getInt("ID");
                this.directory = rs.getBoolean("IS_FOLDER");
                this.resourceDate = rs.getTimestamp("RESOURCE_DATE");
                this.resourceSize = rs.getInt("RESOURCE_SIZE");
            } else {
                this.resourceId = null;
                this.directory = false;
                this.resourceDate = null;
                this.resourceSize = null;
            }
        } catch (SQLException x) {
        } finally {
            closeQuitely(stmt);
            closeQuitely(rs);
        }
    }

    /**
     * Normalize separate character. Separate character should be '/' always.
     */
    public final static String normalizeSeparateChar(final String pathName) {
        String normalizedPathName = pathName.replace(File.separatorChar, '/');
        normalizedPathName = normalizedPathName.replace('\\', '/');
        return normalizedPathName;
    }

    /**
     * Get the physical canonical file name. It works like
     * File.getCanonicalPath().
     *
     * @param rootDir
     *            The root directory.
     * @param currDir
     *            The current directory. It will always be with respect to the
     *            root directory.
     * @param fileName
     *            The input file name.
     * @return The return string will always begin with the root directory. It
     *         will never be null.
     */
    public final static String getPhysicalName(final String rootDir,
            final String currDir, final String fileName) {
        return getPhysicalName(rootDir, currDir, fileName, false);
    }

    public final static String getPhysicalName(final String rootDir,
            final String currDir, final String fileName,
            final boolean caseInsensitive) {

        // get the starting directory
        String normalizedRootDir = normalizeSeparateChar(rootDir);
        if (normalizedRootDir.charAt(normalizedRootDir.length() - 1) != '/') {
            normalizedRootDir += '/';
        }

        String normalizedFileName = normalizeSeparateChar(fileName);
        String resArg;
        String normalizedCurrDir = currDir;
        if (normalizedFileName.charAt(0) != '/') {
            if (normalizedCurrDir == null) {
                normalizedCurrDir = "/";
            }
            if (normalizedCurrDir.length() == 0) {
                normalizedCurrDir = "/";
            }

            normalizedCurrDir = normalizeSeparateChar(normalizedCurrDir);

            if (normalizedCurrDir.charAt(0) != '/') {
                normalizedCurrDir = '/' + normalizedCurrDir;
            }
            if (normalizedCurrDir.charAt(normalizedCurrDir.length() - 1) != '/') {
                normalizedCurrDir += '/';
            }

            resArg = normalizedRootDir + normalizedCurrDir.substring(1);
        } else {
            resArg = normalizedRootDir;
        }

        // strip last '/'
        if (resArg.charAt(resArg.length() - 1) == '/') {
            resArg = resArg.substring(0, resArg.length() - 1);
        }

        // replace ., ~ and ..
        // in this loop resArg will never end with '/'
        StringTokenizer st = new StringTokenizer(normalizedFileName, "/");
        while (st.hasMoreTokens()) {
            String tok = st.nextToken();

            // . => current directory
            if (tok.equals(".")) {
                continue;
            }

            // .. => parent directory (if not root)
            if (tok.equals("..")) {
                if (resArg.startsWith(normalizedRootDir)) {
                    int slashIndex = resArg.lastIndexOf('/');
                    if (slashIndex != -1) {
                        resArg = resArg.substring(0, slashIndex);
                    }
                }
                continue;
            }

            // ~ => home directory (in this case the root directory)
            if (tok.equals("~")) {
                resArg = normalizedRootDir.substring(0, normalizedRootDir.length() - 1);
                continue;
            }

//            if (caseInsensitive) {
//                File[] matches = new File(resArg)
//                        .listFiles(new NameEqualsFileFilter(tok, true));
//
//                if (matches != null && matches.length > 0) {
//                    tok = matches[0].getName();
//                }
//            }
//
            resArg = resArg + '/' + tok;
        }

        // add last slash if necessary
        if ((resArg.length()) + 1 == normalizedRootDir.length()) {
            resArg += '/';
        }

        // final check
        if (!resArg.regionMatches(0, normalizedRootDir, 0, normalizedRootDir.length())) {
            resArg = normalizedRootDir;
        }

        return resArg;
    }

    @Override
    public boolean equals(Object obj) {
        boolean res = false;
        if (obj != null && obj instanceof DbFile) {
            DbFile otherDbFile = (DbFile) obj;
            if (otherDbFile.getAbsolutePath().equals(this.getAbsolutePath())) {
                res = true;
            }
        }
        return res;
    }

    class DbFileInputStream extends InputStream {

        public DbFileInputStream() {
        }

        private InputStream embeddedInputStream;

        private Statement stmt;

        private ResultSet rs;

        @Override
        public int read() throws IOException {
            if (null == embeddedInputStream) {
                setupEmbeddedInputStream();
            }
            return embeddedInputStream.read();
        }

        private void setupEmbeddedInputStream() throws IOException {
            try {
                stmt = createConnection().createStatement();
                rs = stmt.executeQuery("SELECT R.RESOURCE_DATA FROM LIBRARY_RESOURCE R WHERE R.PATH = '" + path + "' AND R.NAME = '" + name + "'");
                if (rs.next()) {
                    embeddedInputStream = rs.getBinaryStream("RESOURCE_DATA");
                } else {
                    throw new IOException("no data available");
                }
            } catch (SQLException sQLException) {
                throw new IOException("failed to get input stream", sQLException);
            }
        }

        @Override
        public void close() throws IOException {
            if (null != embeddedInputStream) {
                embeddedInputStream.close();
                embeddedInputStream = null;
            }
            closeQuitely(stmt);
            closeQuitely(rs);
        }
    }

    class DbFileOutputStream extends OutputStream {

        public DbFileOutputStream() {
        }

        ByteArrayOutputStream bis = new ByteArrayOutputStream();

        @Override
        public void write(int b) throws IOException {
            bis.write(b);
        }

        @Override
        public void close() throws IOException {

            if (null != bis) {
                Statement stmt = null;
                PreparedStatement pstmt = null;
                ResultSet rs = null;
                Integer parentId = null;
                Integer id = null;
                try {
                    stmt = createConnection().createStatement();
                    rs = stmt.executeQuery("SELECT R.ID FROM LIBRARY_RESOURCE R WHERE R.PATH = '" + path + "' AND R.NAME = '" + name + "'");
                    if (rs.next()) {
                        id = rs.getInt("id");
                        pstmt = createConnection().prepareStatement("UPDATE LIBRARY_RESOURCE R SET " +
                                "RESOURCE_DATE = CURRENT_TIMESTAMP, " +
                                "RESOURCE_SIZE = ?, " +
                                "RESOURCE_DATA = ? " +
                                "WHERE " +
                                "R.ID = ?");
                        pstmt.setInt(1, bis.size());
                        pstmt.setBytes(2, bis.toByteArray());
                        pstmt.setInt(3, id);
                        pstmt.execute();
                    } else {
                        String parentPath = getDirectoryFromPath(path);
                        String parentName = getNameFromPath(path);
                        rs = stmt.executeQuery("SELECT R.ID FROM LIBRARY_RESOURCE R WHERE R.PATH = '" + parentPath + "' AND R.NAME = '" + parentName + "'");
                        if (rs.next()) {
                            parentId = rs.getInt("ID");
                        } else {
                            throw new IOException("failed to find parent directory");
                        }
                        pstmt = createConnection().prepareStatement("INSERT INTO LIBRARY_RESOURCE (" +
                                "PARENT_ID, " +
                                "PATH, " +
                                "NAME, " +
                                "IS_FOLDER," +
                                "IS_READONLY," +
                                "RESOURCE_DATE," +
                                "RESOURCE_SIZE," +
                                "RESOURCE_DATA " +
                                ") VALUES ( " +
                                "?, " +
                                "?, " +
                                "?, " +
                                "0, " +
                                "0, " +
                                "CURRENT_TIMESTAMP, " +
                                "? ," +
                                "? " +
                                ")");
                        pstmt.setInt(1, parentId);
                        pstmt.setString(2, path);
                        pstmt.setString(3, name);
                        pstmt.setInt(4, bis.size());
                        pstmt.setBytes(5, bis.toByteArray());
                        pstmt.execute();
                    }
                } catch (SQLException sQLException) {
                    throw new IOException("failed to finalize data to stream", sQLException);
                } finally {
                    closeQuitely(stmt);
                    closeQuitely(pstmt);
                    closeQuitely(rs);
                    if (null != bis) {
                        bis.close();
                        bis = null;
                    }
                }
            }
        }
    }
}
