/*
 * Copyright (C) 2014 Indeed Inc.
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * 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.indeed.imhotep.io.caching;

import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ListObjectsRequest;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectInputStream;
import com.amazonaws.services.s3.model.S3ObjectSummary;

public class S3RemoteFileSystem extends RemoteFileSystem {
    private static final Logger log = Logger.getLogger(S3RemoteFileSystem.class);
    
    private String mountPoint;
    private RemoteFileSystemMounter mounter;
    private String s3bucket;
    private String s3prefix;
    private AmazonS3Client client;

    public S3RemoteFileSystem(Map<String,Object> settings, 
                              RemoteFileSystem parent,
                              RemoteFileSystemMounter mounter) {
        final String s3key;
        final String s3secret;
        final BasicAWSCredentials cred;
        
        mountPoint = (String)settings.get("mountpoint");
        if (! mountPoint.endsWith(DELIMITER)) {
            /* add delimiter to the end */
            mountPoint = mountPoint + DELIMITER;
        }
        mountPoint = mounter.getRootMountPoint() + mountPoint;
        mountPoint = mountPoint.replace("//", "/");
        
        this.mounter = mounter;
        
        s3bucket = (String)settings.get("s3-bucket");
        s3prefix = (String)settings.get("s3-prefix");
        if (s3prefix != null) {
            s3prefix = RemoteFileSystem.cleanupPath(s3prefix.trim());
        }
        s3key = (String)settings.get("s3-key");
        s3secret = (String)settings.get("s3-secret");
        cred = new BasicAWSCredentials(s3key, s3secret);

        client = new AmazonS3Client(cred);
    }
    
    private String getS3path(String path) {
        
        if (s3prefix != null && !s3prefix.isEmpty()) {
            if (path.isEmpty()) {
                return s3prefix;
            } else {
                return s3prefix + DELIMITER + path;
            }
        }
        return path;
    }
    
    @Override
    public String getMountPoint() {
        return this.mountPoint;
    }
    
    @Override
    public File loadFile(String path) throws IOException {
        final File file;
        
        file = File.createTempFile("imhotep.s3.", ".cachedFile");
        copyFileInto(path, file);
        return file;
    }

    public String getRemotePath(String path) {
        String remotePath;
        final String mountPointNoDelim;
        
        mountPointNoDelim = mountPoint.substring(0, mountPoint.length() - DELIMITER.length());
        if (path.equals(mountPointNoDelim) || path.equals(mountPoint)) {
            return "";
        } else if (path.startsWith(mountPoint)) {
            remotePath = path.substring(mountPoint.length());
            remotePath = RemoteFileSystem.cleanupPath(remotePath);
            return remotePath;
        }
        
        throw new IllegalArgumentException("File is not located off the \"mount point\".");
    }

    @Override
    public void copyFileInto(String fullPath, File localFile) throws IOException {
        final String relativePath = mounter.getMountRelativePath(fullPath, mountPoint);
        final String s3path = getS3path(relativePath);
        final ObjectMetadata metadata;
        
        try {
            metadata = client.getObject(new GetObjectRequest(s3bucket, s3path), localFile);
        } catch(AmazonS3Exception e) {
            throw new IOException(e);
        }
    }

    private ObjectMetadata getMetadata(String fullPath) {
        final String relativePath = mounter.getMountRelativePath(fullPath, mountPoint);
        final String s3path = getS3path(relativePath);
        final ObjectMetadata metadata;
        
        try {
            metadata = client.getObjectMetadata(s3bucket, s3path);
        } catch(AmazonServiceException e) {
            return null;
        }
        
        return metadata;
    }
    
    private ObjectListing getListing(String s3path, int maxResults, boolean recursive) {
        final ListObjectsRequest reqParams;
        final ObjectListing listing;

        reqParams = new ListObjectsRequest();
        reqParams.setBucketName(s3bucket);
        reqParams.setPrefix(s3path);
        if (maxResults > 0) {
            reqParams.setMaxKeys(maxResults);
        }
        if (! recursive) {
            reqParams.setDelimiter(DELIMITER);
        }
        
        listing = client.listObjects(reqParams);
        
        return listing;
    }
    
    /*
     * Returns null if file not found
     * 
     */
    @Override
    public RemoteFileInfo stat(String fullPath) {
        final String relativePath = mounter.getMountRelativePath(fullPath, mountPoint);
        final String s3path = getS3path(relativePath);
        final ObjectListing listing;
        final int type;
        
        listing = getListing(s3path, 1, true);
        
        if (listing.getObjectSummaries().size() == 0) {
            return null;
        }
        
        final String key;
        key = listing.getObjectSummaries().get(0).getKey();
        
        if (key.equals(s3path)) {
            type = RemoteFileInfo.TYPE_FILE;
        } else if (s3path.isEmpty()) {
            type = RemoteFileInfo.TYPE_DIR;
        } else if (key.startsWith(s3path + DELIMITER)) {
            type = RemoteFileInfo.TYPE_DIR;
        } else {
            return null;
        }
       
        return new RemoteFileInfo(relativePath, type);
    }

    private List<String> getFilenamesFromListing(ObjectListing listing, String prefix) {
        List<String> results = new ArrayList<String>(100);

        for (S3ObjectSummary summary : listing.getObjectSummaries()) {
            final String key = summary.getKey();
            final String filename;
            
            filename = key.substring(prefix.length());
            if (filename.length() == 0 || filename.contains(DELIMITER)) {
                log.error("Error parsing S3 object Key.  Key: " + key);
                continue;
            }
            results.add(filename);
        }
        
        return results;
    }
    
    private List<String> getCommonPrefixFromListing(ObjectListing listing, String prefix) {
        List<String> results = new ArrayList<String>(100);

        for (String commonPrefix : listing.getCommonPrefixes()) {
            final String dirname;
            
            /* remove prefix and trailing delimiter */
            dirname = commonPrefix.substring(prefix.length(), 
                                             commonPrefix.length() - DELIMITER.length());
            if (dirname.length() == 0 || dirname.contains(DELIMITER)) {
                log.error("Error parsing S3 object prefix.  Prefix: " + commonPrefix);
                continue;
            }
            results.add(dirname);
        }
        
        return results;
    }

    @Override
    public List<RemoteFileInfo> readDir(String fullPath) {
        final String relativePath = mounter.getMountRelativePath(fullPath, mountPoint);
        String s3path = getS3path(relativePath);
        final List<RemoteFileInfo> results = new ArrayList<RemoteFileInfo>(100);
        ObjectListing listing;
        
        if (!s3path.isEmpty()) {
            s3path += DELIMITER;
        }
        
        /* grab first set of keys for the object we found */
        listing = getListing(s3path, -1, false);
        for (String filename : getFilenamesFromListing(listing, s3path)) {
            results.add(new RemoteFileInfo(filename, RemoteFileInfo.TYPE_FILE));
        }
        /* add the common prefixes */
        for (String dirname : getCommonPrefixFromListing(listing, s3path)) {
            results.add(new RemoteFileInfo(dirname, RemoteFileInfo.TYPE_DIR));
        }
        
        
        /* loop until all the keys have been read */
        while(listing.isTruncated()) {
            listing = client.listNextBatchOfObjects(listing);
            
            for (String filename : getFilenamesFromListing(listing, s3path)) {
                results.add(new RemoteFileInfo(filename, RemoteFileInfo.TYPE_FILE));
            }
            /* add the common prefixes */
            for (String dirname : getCommonPrefixFromListing(listing, s3path)) {
                results.add(new RemoteFileInfo(dirname, RemoteFileInfo.TYPE_DIR));
            }
        }

        if (results.size() == 0) {
            return null;
        }
        
        return results;
    }

    @Override
    public Map<String,File> loadDirectory(String fullPath, File location) throws IOException {
        final String relativePath = mounter.getMountRelativePath(fullPath, mountPoint);
        String s3path = getS3path(relativePath);
        ObjectListing listing;
        final Map<String,File> results;
        
        if (!s3path.isEmpty()) {
            s3path += DELIMITER;
        }
        
        results = new HashMap<String,File>(100);
        
        if (location == null) {
            location = File.createTempFile("s3", "remoteFile");
            location.delete();
            location.mkdir();
        }
        
        /* grab first set of keys for the object we found */
        listing = getListing(s3path, -1, true);
        /* load all of the objects */
        for (String filename : getFilenamesFromListing(listing, s3path)) {
            final String fname = joinPaths(fullPath, filename);
            final File localFile;

            localFile = new File(location, filename);
            /* create all the directories on the path to the file */
            localFile.getParentFile().mkdirs();

            /* download file */
            copyFileInto(fname, localFile);
            
            results.put(fname, localFile);
        }
        
        
        /* loop until all the keys have been read */
        while(listing.isTruncated()) {
            listing = client.listNextBatchOfObjects(listing);
            
            /* load all of the objects */
            for (String filename : getFilenamesFromListing(listing, s3path)) {
                final String fname = joinPaths(fullPath, filename);
                final File localFile;

                localFile = new File(location, filename);
                /* create all the directories on the path to the file */
                localFile.getParentFile().mkdirs();

                /* download file */
                copyFileInto(fname, localFile);
                
                results.put(fname, localFile);
            }
        }

        return results;
    }
    
    private String joinPaths(String dirPath, String filePath) {
        String path;
        
        if (dirPath.isEmpty()) {
            path = filePath;
        } else {
            path = dirPath + filePath;
        }
        
        return path;
    }

    @Override
    public InputStream getInputStreamForFile(String fullPath,
                                             long startOffset,
                                             long maxReadLength) throws IOException {
        final String relativePath = mounter.getMountRelativePath(fullPath, mountPoint);
        final String s3path = getS3path(relativePath);
        final S3Object s3obj;
        final S3ObjectInputStream is;
        final GetObjectRequest request;
        
        request = new GetObjectRequest(s3bucket, s3path);
        
        if (maxReadLength != -1) {
            request.setRange(startOffset, startOffset + maxReadLength);
        }

        try {
            s3obj = client.getObject(request);
            is = s3obj.getObjectContent();
            return new AutoAbortingS3InputStream(is, maxReadLength);
        } catch(AmazonS3Exception e) {
            throw new IOException(e);
        }
    }
    
    private static final class AutoAbortingS3InputStream extends FilterInputStream {
        private long amtRead;
        private long size;

        public AutoAbortingS3InputStream(S3ObjectInputStream baseIS,
                                         long maxReadLength) {
            super(baseIS);
            this.amtRead = 0;
            this.size = maxReadLength;
        }
        

        @Override
        public int read() throws IOException {
            return super.read();
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            final int read = super.read(b, off, len);
            amtRead += read;
            return read;
        }

        @Override
        public long skip(long n) throws IOException {
            amtRead += n;
            return super.skip(n);
        }

        @Override
        public synchronized void mark(int readlimit) {
            throw new UnsupportedOperationException();
        }

        @Override
        public synchronized void reset() throws IOException {
            throw new UnsupportedOperationException();
        }

        @Override
        public boolean markSupported() {
            return false;
        }

        @Override
        public void close() throws IOException {
            final long amtLeft;
            
            amtLeft = size - amtRead;
            if (amtLeft > 12000) {  // ~ 4 packets
                ((S3ObjectInputStream)in).abort();
            } else {
                in.close();
            }
        }
    }
}