/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you 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 org.apache.cordova.file;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;

import org.apache.cordova.CordovaInterface;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.util.Base64;
import android.net.Uri;

public class LocalFilesystem extends Filesystem {

	private String fsRoot;
	private CordovaInterface cordova;

	public LocalFilesystem(String name, CordovaInterface cordova, String fsRoot) {
		this.name = name;
		this.fsRoot = fsRoot;
		this.cordova = cordova;
	}

	public String filesystemPathForFullPath(String fullPath) {
	    String path = new File(this.fsRoot, fullPath).toString();
        int questionMark = path.indexOf("?");
        if (questionMark >= 0) {
          path = path.substring(0, questionMark);
        }
	    if (path.length() > 1 && path.endsWith("/")) {
	      path = path.substring(0, path.length()-1);
	    }
	    return path;
	}
	
	@Override
	public String filesystemPathForURL(LocalFilesystemURL url) {
		return filesystemPathForFullPath(url.fullPath);
	}

	private String fullPathForFilesystemPath(String absolutePath) {
		if (absolutePath != null && absolutePath.startsWith(this.fsRoot)) {
			return absolutePath.substring(this.fsRoot.length());
		}
		return null;
	}

	protected LocalFilesystemURL URLforFullPath(String fullPath) {
	    if (fullPath != null) {
	    	if (fullPath.startsWith("/")) {
	    		return new LocalFilesystemURL(LocalFilesystemURL.FILESYSTEM_PROTOCOL + "://localhost/"+this.name+fullPath);
	    	}
	        return new LocalFilesystemURL(LocalFilesystemURL.FILESYSTEM_PROTOCOL + "://localhost/"+this.name+"/"+fullPath);
	    }
	    return null;
		
	}
	
	@Override
	public LocalFilesystemURL URLforFilesystemPath(String path) {
	    return this.URLforFullPath(this.fullPathForFilesystemPath(path));
	}

	protected String normalizePath(String rawPath) {
	    // If this is an absolute path, trim the leading "/" and replace it later
	    boolean isAbsolutePath = rawPath.startsWith("/");
	    if (isAbsolutePath) {
	        rawPath = rawPath.substring(1);
	    }
	    ArrayList<String> components = new ArrayList<String>(Arrays.asList(rawPath.split("/")));
	    for (int index = 0; index < components.size(); ++index) {
	        if (components.get(index).equals("..")) {
	            components.remove(index);
	            if (index > 0) {
	                components.remove(index-1);
	                --index;
	            }
	        }
	    }
	    StringBuilder normalizedPath = new StringBuilder();
	    for(String component: components) {
	    	normalizedPath.append("/");
	    	normalizedPath.append(component);
	    }
	    if (isAbsolutePath) {
	    	return normalizedPath.toString();
	    } else {
	    	return normalizedPath.toString().substring(1);
	    }


	}

	
	@Override
    public JSONObject makeEntryForFile(File file) throws JSONException {
    	String path = this.fullPathForFilesystemPath(file.getAbsolutePath());
    	if (path != null) {
    		return makeEntryForPath(path, this.name, file.isDirectory(), Uri.fromFile(file).toString());
    	}
    	return null;
    }

	@Override
	public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOException {
      File fp = new File(filesystemPathForURL(inputURL));

      if (!fp.exists()) {
          throw new FileNotFoundException();
      }
      if (!fp.canRead()) {
          throw new IOException();
      }
      try {
    	  JSONObject entry = new JSONObject();
    	  entry.put("isFile", fp.isFile());
    	  entry.put("isDirectory", fp.isDirectory());
    	  entry.put("name", fp.getName());
    	  entry.put("fullPath", inputURL.fullPath);
    	  // The file system can't be specified, as it would lead to an infinite loop.
    	  // But we can specify the name of the FS, and the rest can be reconstructed
    	  // in JS.
    	  entry.put("filesystemName", inputURL.filesystemName);
    	  // Backwards compatibility
    	  entry.put("filesystem", "temporary".equals(name) ? 0 : 1);
    	  entry.put("nativeURL", Uri.fromFile(fp).toString());
          return entry;
      } catch (JSONException e) {
    	  throw new IOException();
      }
	}

	@Override
	public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL,
			String path, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
        boolean create = false;
        boolean exclusive = false;

        if (options != null) {
            create = options.optBoolean("create");
            if (create) {
                exclusive = options.optBoolean("exclusive");
            }
        }

        // Check for a ":" character in the file to line up with BB and iOS
        if (path.contains(":")) {
            throw new EncodingException("This path has an invalid \":\" in it.");
        }

        LocalFilesystemURL requestedURL;
        
        // Check whether the supplied path is absolute or relative
        if (path.startsWith("/")) {
        	requestedURL = URLforFilesystemPath(path);
        } else {
        	requestedURL = URLforFullPath(normalizePath(inputURL.fullPath + "/" + path));
        }
        
        File fp = new File(this.filesystemPathForURL(requestedURL));

        if (create) {
            if (exclusive && fp.exists()) {
                throw new FileExistsException("create/exclusive fails");
            }
            if (directory) {
                fp.mkdir();
            } else {
                fp.createNewFile();
            }
            if (!fp.exists()) {
                throw new FileExistsException("create fails");
            }
        }
        else {
            if (!fp.exists()) {
                throw new FileNotFoundException("path does not exist");
            }
            if (directory) {
                if (fp.isFile()) {
                    throw new TypeMismatchException("path doesn't exist or is file");
                }
            } else {
                if (fp.isDirectory()) {
                    throw new TypeMismatchException("path doesn't exist or is directory");
                }
            }
        }

        // Return the directory
        return makeEntryForPath(requestedURL.fullPath, requestedURL.filesystemName, directory, Uri.fromFile(fp).toString());
	}

	@Override
	public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws InvalidModificationException {

        File fp = new File(filesystemPathForURL(inputURL));

        // You can't delete a directory that is not empty
        if (fp.isDirectory() && fp.list().length > 0) {
            throw new InvalidModificationException("You can't delete a directory that is not empty.");
        }

        return fp.delete();
	}

	@Override
	public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws FileExistsException {
        File directory = new File(filesystemPathForURL(inputURL));
    	return removeDirRecursively(directory);
	}
	
	protected boolean removeDirRecursively(File directory) throws FileExistsException {
        if (directory.isDirectory()) {
            for (File file : directory.listFiles()) {
                removeDirRecursively(file);
            }
        }

        if (!directory.delete()) {
            throw new FileExistsException("could not delete: " + directory.getName());
        } else {
            return true;
        }
	}

	@Override
	public JSONArray readEntriesAtLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
        File fp = new File(filesystemPathForURL(inputURL));

        if (!fp.exists()) {
            // The directory we are listing doesn't exist so we should fail.
            throw new FileNotFoundException();
        }

        JSONArray entries = new JSONArray();

        if (fp.isDirectory()) {
            File[] files = fp.listFiles();
            for (int i = 0; i < files.length; i++) {
                if (files[i].canRead()) {
                    try {
						entries.put(makeEntryForPath(fullPathForFilesystemPath(files[i].getAbsolutePath()), inputURL.filesystemName, files[i].isDirectory(), Uri.fromFile(files[i]).toString()));
					} catch (JSONException e) {
					}
                }
            }
        }

        return entries;
	}

	@Override
	public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
        File file = new File(filesystemPathForURL(inputURL));

        if (!file.exists()) {
            throw new FileNotFoundException("File at " + inputURL.URL + " does not exist.");
        }

        JSONObject metadata = new JSONObject();
        try {
            // Ensure that directories report a size of 0
        	metadata.put("size", file.isDirectory() ? 0 : file.length());
        	metadata.put("type", FileHelper.getMimeType(file.getAbsolutePath(), cordova));
        	metadata.put("name", file.getName());
        	metadata.put("fullPath", inputURL.fullPath);
        	metadata.put("lastModifiedDate", file.lastModified());
        } catch (JSONException e) {
        	return null;
        }
        return metadata;
	}

    /**
     * Check to see if the user attempted to copy an entry into its parent without changing its name,
     * or attempted to copy a directory into a directory that it contains directly or indirectly.
     *
     * @param srcDir
     * @param destinationDir
     * @return
     */
    private boolean isCopyOnItself(String src, String dest) {

        // This weird test is to determine if we are copying or moving a directory into itself.
        // Copy /sdcard/myDir to /sdcard/myDir-backup is okay but
        // Copy /sdcard/myDir to /sdcard/myDir/backup should throw an INVALID_MODIFICATION_ERR
        if (dest.startsWith(src) && dest.indexOf(File.separator, src.length() - 1) != -1) {
            return true;
        }

        return false;
    }

    /**
     * Copy a file
     *
     * @param srcFile file to be copied
     * @param destFile destination to be copied to
     * @return a FileEntry object
     * @throws IOException
     * @throws InvalidModificationException
     * @throws JSONException
     */
    private JSONObject copyFile(File srcFile, File destFile) throws IOException, InvalidModificationException, JSONException {
        // Renaming a file to an existing directory should fail
        if (destFile.exists() && destFile.isDirectory()) {
            throw new InvalidModificationException("Can't rename a file to a directory");
        }

        copyAction(srcFile, destFile);

        return makeEntryForFile(destFile);
    }

    /**
     * Moved this code into it's own method so moveTo could use it when the move is across file systems
     */
    private void copyAction(File srcFile, File destFile)
            throws FileNotFoundException, IOException {
        FileInputStream istream = new FileInputStream(srcFile);
        FileOutputStream ostream = new FileOutputStream(destFile);
        FileChannel input = istream.getChannel();
        FileChannel output = ostream.getChannel();

        try {
            input.transferTo(0, input.size(), output);
        } finally {
            istream.close();
            ostream.close();
            input.close();
            output.close();
        }
    }

    /**
     * Copy a directory
     *
     * @param srcDir directory to be copied
     * @param destinationDir destination to be copied to
     * @return a DirectoryEntry object
     * @throws JSONException
     * @throws IOException
     * @throws NoModificationAllowedException
     * @throws InvalidModificationException
     */
    private JSONObject copyDirectory(File srcDir, File destinationDir) throws JSONException, IOException, NoModificationAllowedException, InvalidModificationException {
        // Renaming a file to an existing directory should fail
        if (destinationDir.exists() && destinationDir.isFile()) {
            throw new InvalidModificationException("Can't rename a file to a directory");
        }

        // Check to make sure we are not copying the directory into itself
        if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) {
            throw new InvalidModificationException("Can't copy itself into itself");
        }

        // See if the destination directory exists. If not create it.
        if (!destinationDir.exists()) {
            if (!destinationDir.mkdir()) {
                // If we can't create the directory then fail
                throw new NoModificationAllowedException("Couldn't create the destination directory");
            }
        }
        

        for (File file : srcDir.listFiles()) {
            File destination = new File(destinationDir.getAbsoluteFile() + File.separator + file.getName());
            if (file.isDirectory()) {
                copyDirectory(file, destination);
            } else {
                copyFile(file, destination);
            }
        }

        return makeEntryForFile(destinationDir);
    }

    /**
     * Move a file
     *
     * @param srcFile file to be copied
     * @param destFile destination to be copied to
     * @return a FileEntry object
     * @throws IOException
     * @throws InvalidModificationException
     * @throws JSONException
     */
    private JSONObject moveFile(File srcFile, File destFile) throws IOException, JSONException, InvalidModificationException {
        // Renaming a file to an existing directory should fail
        if (destFile.exists() && destFile.isDirectory()) {
            throw new InvalidModificationException("Can't rename a file to a directory");
        }

        // Try to rename the file
        if (!srcFile.renameTo(destFile)) {
            // Trying to rename the file failed.  Possibly because we moved across file system on the device.
            // Now we have to do things the hard way
            // 1) Copy all the old file
            // 2) delete the src file
            copyAction(srcFile, destFile);
            if (destFile.exists()) {
                srcFile.delete();
            } else {
                throw new IOException("moved failed");
            }
        }

        return makeEntryForFile(destFile);
    }

    /**
     * Move a directory
     *
     * @param srcDir directory to be copied
     * @param destinationDir destination to be copied to
     * @return a DirectoryEntry object
     * @throws JSONException
     * @throws IOException
     * @throws InvalidModificationException
     * @throws NoModificationAllowedException
     * @throws FileExistsException
     */
    private JSONObject moveDirectory(File srcDir, File destinationDir) throws IOException, JSONException, InvalidModificationException, NoModificationAllowedException, FileExistsException {
        // Renaming a file to an existing directory should fail
        if (destinationDir.exists() && destinationDir.isFile()) {
            throw new InvalidModificationException("Can't rename a file to a directory");
        }

        // Check to make sure we are not copying the directory into itself
        if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) {
            throw new InvalidModificationException("Can't move itself into itself");
        }

        // If the destination directory already exists and is empty then delete it.  This is according to spec.
        if (destinationDir.exists()) {
            if (destinationDir.list().length > 0) {
                throw new InvalidModificationException("directory is not empty");
            }
        }

        // Try to rename the directory
        if (!srcDir.renameTo(destinationDir)) {
            // Trying to rename the directory failed.  Possibly because we moved across file system on the device.
            // Now we have to do things the hard way
            // 1) Copy all the old files
            // 2) delete the src directory
            copyDirectory(srcDir, destinationDir);
            if (destinationDir.exists()) {
                removeDirRecursively(srcDir);
            } else {
                throw new IOException("moved failed");
            }
        }

        return makeEntryForFile(destinationDir);
    }
	
	@Override
	public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName,
			Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException {

		// Check to see if the destination directory exists
        String newParent = this.filesystemPathForURL(destURL);
        File destinationDir = new File(newParent);
        if (!destinationDir.exists()) {
            // The destination does not exist so we should fail.
            throw new FileNotFoundException("The source does not exist");
        }
        
	    if (LocalFilesystem.class.isInstance(srcFs)) {
	        /* Same FS, we can shortcut with NSFileManager operations */

            // Figure out where we should be copying to
            final LocalFilesystemURL destinationURL = makeDestinationURL(newName, srcURL, destURL);

	        String srcFilesystemPath = srcFs.filesystemPathForURL(srcURL);
            File sourceFile = new File(srcFilesystemPath);
            String destFilesystemPath = this.filesystemPathForURL(destinationURL);
            File destinationFile = new File(destFilesystemPath);

            if (!sourceFile.exists()) {
	            // The file/directory we are copying doesn't exist so we should fail.
	            throw new FileNotFoundException("The source does not exist");
	        }

	        // Check to see if source and destination are the same file
            if (sourceFile.getAbsolutePath().equals(destinationFile.getAbsolutePath())) {
	            throw new InvalidModificationException("Can't copy a file onto itself");
	        }

            if (sourceFile.isDirectory()) {
	            if (move) {
                    return moveDirectory(sourceFile, destinationFile);
	            } else {
                    return copyDirectory(sourceFile, destinationFile);
	            }
	        } else {
	            if (move) {
                    return moveFile(sourceFile, destinationFile);
	            } else {
                    return copyFile(sourceFile, destinationFile);
	            }
	        }
	    	
	    } else {
	        // Need to copy the hard way
            return super.copyFileToURL(destURL, newName, srcFs, srcURL, move);
    	}
	}

	@Override
    public void readFileAtURL(LocalFilesystemURL inputURL, long start, long end,
			ReadFileCallback readFileCallback) throws IOException {

		File file = new File(this.filesystemPathForURL(inputURL));
        String contentType = FileHelper.getMimeTypeForExtension(file.getAbsolutePath());
		
        if (end < 0) {
            end = file.length();
        }
        long numBytesToRead = end - start;

        InputStream rawInputStream = new FileInputStream(file);
		try {
			if (start > 0) {
                rawInputStream.skip(start);
			}
            LimitedInputStream inputStream = new LimitedInputStream(rawInputStream, numBytesToRead);
            readFileCallback.handleData(inputStream, contentType);
		} finally {
            rawInputStream.close();
		}
	}
    
	@Override
	public long writeToFileAtURL(LocalFilesystemURL inputURL, String data,
			int offset, boolean isBinary) throws IOException, NoModificationAllowedException {

        boolean append = false;
        if (offset > 0) {
            this.truncateFileAtURL(inputURL, offset);
            append = true;
        }

        byte[] rawData;
        if (isBinary) {
            rawData = Base64.decode(data, Base64.DEFAULT);
        } else {
            rawData = data.getBytes();
        }
        ByteArrayInputStream in = new ByteArrayInputStream(rawData);
        try
        {
        	byte buff[] = new byte[rawData.length];
            FileOutputStream out = new FileOutputStream(this.filesystemPathForURL(inputURL), append);
            try {
            	in.read(buff, 0, buff.length);
            	out.write(buff, 0, rawData.length);
            	out.flush();
            } finally {
            	// Always close the output
            	out.close();
            }
        }
        catch (NullPointerException e)
        {
            // This is a bug in the Android implementation of the Java Stack
            NoModificationAllowedException realException = new NoModificationAllowedException(inputURL.toString());
            throw realException;
        }

        return rawData.length;
	}

	@Override
	public long truncateFileAtURL(LocalFilesystemURL inputURL, long size) throws IOException {
        File file = new File(filesystemPathForURL(inputURL));

        if (!file.exists()) {
            throw new FileNotFoundException("File at " + inputURL.URL + " does not exist.");
        }
        
        RandomAccessFile raf = new RandomAccessFile(filesystemPathForURL(inputURL), "rw");
        try {
            if (raf.length() >= size) {
                FileChannel channel = raf.getChannel();
                channel.truncate(size);
                return size;
            }

            return raf.length();
        } finally {
            raf.close();
        }


	}

	@Override
	public boolean canRemoveFileAtLocalURL(LocalFilesystemURL inputURL) {
		String path = filesystemPathForURL(inputURL);
		File file = new File(path);
		return file.exists();
	}

	@Override
	OutputStream getOutputStreamForURL(LocalFilesystemURL inputURL) throws FileNotFoundException {
		String path = filesystemPathForURL(inputURL);
		File file = new File(path);
		FileOutputStream os = new FileOutputStream(file);
		return os;
	}

}
