package railo.commons.io.res.type.file;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import railo.commons.cli.Command;
import railo.commons.io.IOUtil;
import railo.commons.io.ModeUtil;
import railo.commons.io.SystemUtil;
import railo.commons.io.res.ContentType;
import railo.commons.io.res.Resource;
import railo.commons.io.res.ResourceProvider;
import railo.commons.io.res.filter.ResourceFilter;
import railo.commons.io.res.filter.ResourceNameFilter;
import railo.commons.io.res.util.ResourceOutputStream;
import railo.commons.io.res.util.ResourceUtil;

/**
 * Implementation og Resource for the local filesystem (java.io.File)
 */
public final class FileResource extends File implements Resource {

	private final FileResourceProvider provider;

	/**
	 * Constructor for the factory
	 * @param pathname
	 */
	FileResource(FileResourceProvider provider,String pathname) {
		super(pathname);
		this.provider=provider;
	}

	/**
	 * Inner Constr constructor to create parent/child
	 * @param parent
	 * @param child
	 */
	private FileResource(FileResourceProvider provider,File parent, String child) {
		super(parent, child);
		this.provider=provider;
	}


	@Override
	public void copyFrom(Resource res,boolean append) throws IOException {
		IOUtil.copy(res, this.getOutputStream(append),true);
	}

	@Override
	public void copyTo(Resource res,boolean append) throws IOException {
		IOUtil.copy(this, res.getOutputStream(append),true);
	}
	
	@Override
	public Resource getAbsoluteResource() {
		return new FileResource(provider,getAbsolutePath());
	}

	@Override
	public Resource getCanonicalResource() throws IOException {
		return new FileResource(provider,getCanonicalPath());
	}

	@Override
	public Resource getParentResource() {
		String p = getParent();
		if(p==null) return null;
		return new FileResource(provider,p);
	}

	@Override
	public Resource[] listResources() {
		String[] files = list();
		if(files==null) return null;
		
		Resource[] resources=new Resource[files.length];
		for(int i=0;i<files.length;i++) {
			resources[i]=getRealResource(files[i]);
		}
		return resources;
	}
	
	@Override
	public String[] list(ResourceFilter filter) {
		String[] files = list();
		if(files==null) return null;
		
		List list=new ArrayList();
		FileResource res;
		for(int i=0;i<files.length;i++) {
			res=new FileResource(provider,this,files[i]);
			if(filter.accept(res))list.add(files[i]);
		}
		return (String[]) list.toArray(new String[list.size()]);
	}

	@Override
	public Resource[] listResources(ResourceFilter filter) {
		String[] files = list();
		if(files==null) return null;
		
		List list=new ArrayList();
		Resource res;
		for(int i=0;i<files.length;i++) {
			res=getRealResource(files[i]);
			if(filter.accept(res))list.add(res);
		}
		return (Resource[]) list.toArray(new FileResource[list.size()]);
	}
	

	@Override
	public String[] list(ResourceNameFilter filter) {
		String[] files = list();
		if(files==null) return null;
		List list=new ArrayList();
		for(int i=0;i<files.length;i++) {
			if(filter.accept(this,files[i]))list.add(files[i]);
		}
		return (String[]) list.toArray(new String[list.size()]);
	}

	@Override
	public Resource[] listResources(ResourceNameFilter filter) {
		String[] files = list();
		if(files==null) return null;
		
		List list=new ArrayList();
		for(int i=0;i<files.length;i++) {
			if(filter.accept(this,files[i]))list.add(getRealResource(files[i]));
		}
		return (Resource[]) list.toArray(new Resource[list.size()]);
	}

	@Override
	public void moveTo(Resource dest) throws IOException {
		if(this.equals(dest)) return;
		boolean done=false;
		if(dest instanceof File) {
			provider.lock(this);
			try {
				if(dest.exists() && !dest.delete())
					throw new IOException("can't move file "+this.getAbsolutePath()+" cannot remove existing file "+dest.getAbsolutePath());
				
				done=super.renameTo((File)dest);
				/*if(!super.renameTo((File)dest)) {
					throw new IOException("can't move file "+this.getAbsolutePath()+" to destination resource "+dest.getAbsolutePath());
				}*/
			}
			finally {
				provider.unlock(this);
			}
		}
		if(!done) {
			ResourceUtil.checkMoveToOK(this, dest);
			IOUtil.copy(getInputStream(),dest,true);
			if(!this.delete()) {
				throw new IOException("can't delete resource "+this.getAbsolutePath());
			}
		}
	}

	@Override
	public InputStream getInputStream() throws IOException {
		//provider.lock(this);
		provider.read(this);
		try {
			//return new BufferedInputStream(new ResourceInputStream(this,new FileInputStream(this)));
			return new BufferedInputStream(new FileInputStream(this));
		}
		catch(IOException ioe) {
			//provider.unlock(this);
			throw ioe;
		}
	}

	@Override
	public OutputStream getOutputStream() throws IOException {
		return getOutputStream(false);
	}

	@Override
	public OutputStream getOutputStream(boolean append) throws IOException {
		provider.lock(this);
		try {
			if(!super.exists() &&  !super.createNewFile()) {
				throw new IOException("can't create file "+this);
			}
			return new BufferedOutputStream(new ResourceOutputStream(this,new FileOutputStream(this,append)));
		}
		catch(IOException ioe) {
			provider.unlock(this);
			throw ioe;
		}
	}
	
	@Override
	public void createFile(boolean createParentWhenNotExists) throws IOException {
		provider.lock(this);
		try {
			if(createParentWhenNotExists) {
				File p = super.getParentFile();
				if(!p.exists()) p.mkdirs();
			}
			if(!super.createNewFile()) {
				if(super.isFile())	throw new IOException("can't create file "+this+", file already exists");
				throw new IOException("can't create file "+this);
			}
		}
		finally {
			provider.unlock(this);
		}
	}
	
	@Override
	public void remove(boolean alsoRemoveChildren) throws IOException {
		if(alsoRemoveChildren && isDirectory()) {
			Resource[] children = listResources();
			for(int i=0;i<children.length;i++) {
				children[i].remove(alsoRemoveChildren);
			}
		}
		provider.lock(this);
		try {
			if(!super.delete()) {
				if(!super.exists())throw new IOException("can't delete file "+this+", file does not exist");
				if(!super.canWrite())throw new IOException("can't delete file "+this+", no access");
				throw new IOException("can't delete file "+this);
			}
		}
		finally {
			provider.unlock(this);
		}
	}

	@Override
	public String getReal(String realpath) {
		if(realpath.length()<=2) {
			if(realpath.length()==0) return getPath();
			if(realpath.equals(".")) return getPath();
			if(realpath.equals("..")) return getParent();
		}
		return new FileResource(provider,this,realpath).getPath();
	}

	@Override
	public Resource getRealResource(String realpath) {
		if(realpath.length()<=2) {
			if(realpath.length()==0) return this;
			if(realpath.equals(".")) return this;
			if(realpath.equals("..")) return getParentResource();
		}
		return new FileResource(provider,this,realpath);
	}

	public ContentType getContentType() {
		return ResourceUtil.getContentType(this);
	}

	@Override
	public void createDirectory(boolean createParentWhenNotExists) throws IOException {
		provider.lock(this);
		try {
			if(createParentWhenNotExists?!_mkdirs():!super.mkdir()) {
				if(super.isDirectory())	throw new IOException("can't create directory "+this+", directory already exists");
				throw new IOException("can't create directory "+this);
			}
		}
		finally {
			provider.unlock(this);
		}
	}

	@Override
	public ResourceProvider getResourceProvider() {
		return provider;
	}

	@Override
	public boolean isReadable() {
		return canRead();
	}

	@Override
	public boolean isWriteable() {
		return canWrite();
	}

	@Override
	public boolean renameTo(Resource dest) {
		try {
			moveTo(dest);
			return true;
		}
		catch (IOException e) {}
		return false;
	}

	@Override
	public boolean isArchive() {
		return getAttribute(ATTRIBUTE_ARCHIVE);
	}

	@Override
	public boolean isSystem() {
		return getAttribute(ATTRIBUTE_SYSTEM);
	}

	@Override
	public int getMode() {
		if(!exists()) return 0;
		if(SystemUtil.isUnix()) {
			try {
				// TODO geht nur fuer file
				String line = Command.execute("ls -ld "+getPath(),false).getOutput();
				
				line=line.trim();
				line=line.substring(0,line.indexOf(' '));
				//print.ln(getPath());
				return ModeUtil.toOctalMode(line);
				
			} catch (Exception e) {}
		
		}
		int mode=SystemUtil.isWindows() && exists() ?0111:0;
		if(super.canRead())mode+=0444;
		if(super.canWrite())mode+=0222;
		return mode;
	}

	public void setMode(int mode) throws IOException {
		// TODO unter windows mit setReadable usw.
		if(!SystemUtil.isUnix()) return;
        provider.lock(this);
        try {
        	//print.ln(ModeUtil.toStringMode(mode));
            if (Runtime.getRuntime().exec(
              new String[] { "chmod", ModeUtil.toStringMode(mode), getPath() } ).waitFor() != 0)
            throw new IOException("chmod  "+ModeUtil.toStringMode(mode)+" " + toString() + " failed");
        }
    	catch (InterruptedException e) {
    	    throw new IOException("Interrupted waiting for chmod " + toString());
    	}
    	finally {
    		provider.unlock(this);
    	}
	}

	@Override
	public void setArchive(boolean value) throws IOException {
		setAttribute(ATTRIBUTE_ARCHIVE, value);
	}

	@Override
	public void setHidden(boolean value) throws IOException {
		setAttribute(ATTRIBUTE_HIDDEN, value);
	}

	@Override
	public void setSystem(boolean value) throws IOException {
		setAttribute(ATTRIBUTE_SYSTEM, value);
	}

	@Override

	public boolean setReadable(boolean value)  {
		if(!SystemUtil.isUnix()) return false;
		try {
			setMode(ModeUtil.setReadable(getMode(), value));
			return true;
		} 
		catch (IOException e) {
			return false;
		}
	}

	public boolean setWritable(boolean value) {
		// setReadonly
		if(!value){
			try {
				provider.lock(this);
				if(!super.setReadOnly()) 
					throw new IOException("can't set resource read-only");
			}
			catch(IOException ioe){
				return false;
			}
			finally {
				provider.unlock(this);
			}
			return true;
		}
		
		if(SystemUtil.isUnix()) {
//			 need no lock because get/setmode has one
			try {
				setMode(ModeUtil.setWritable(getMode(), value));
			} 
			catch (IOException e) {
				return false;
			}
			return true;
		}
		
		try {
			provider.lock(this);
			Runtime.getRuntime().exec("attrib -R " + getAbsolutePath());
		}
		catch(IOException ioe){
			return false;
		}
		finally {
			provider.unlock(this);
		}
		return true;
	}

	
	
	
	
	
	@Override
	public boolean createNewFile() {
		try {
			provider.lock(this);
			return super.createNewFile();
		} 
		catch (IOException e) {
			return false;
		}
		finally {
			provider.unlock(this);
		}
	}
	
	@Override
	public boolean canRead() {
		try {
			provider.read(this);
		} catch (IOException e) {
			return false;
		}
		return super.canRead();
	}

	@Override
	public boolean canWrite() {
		try {
			provider.read(this);
		} catch (IOException e) {
			return false;
		}
		return super.canWrite();
	}

	@Override
	public boolean delete() {
		try {
			provider.lock(this);
			return super.delete();
		}
		catch (IOException e) {
			return false;
		}
		finally {
			provider.unlock(this);
		}
	}

	@Override
	public boolean exists() {
		try {
			provider.read(this);
		} catch (IOException e) {}
		
		return super.exists();
	}

	

	@Override
	public boolean isAbsolute() {
		try {
			provider.read(this);
		}
		catch (IOException e) {
			return false;
		}
		return super.isAbsolute();
	}

	@Override
	public boolean isDirectory() {
		try {
			provider.read(this);
		} catch (IOException e) {
			return false;
		}
		return super.isDirectory();
	}

	@Override
	public boolean isFile() {
		try {
			provider.read(this);
		} catch (IOException e) {
			return false;
		}
		return super.isFile();
	}

	@Override
	public boolean isHidden() {
		try {
			provider.read(this);
		} catch (IOException e) {
			return false;
		}
		return super.isHidden();
	}

	@Override
	public long lastModified() {
		try {
			provider.read(this);
		} catch (IOException e) {
			return 0;
		}
		return super.lastModified();
	}

	@Override
	public long length() {
		try {
			provider.read(this);
		} catch (IOException e) {
			return 0;
		}
		return super.length();
	}

	@Override
	public String[] list() {
		try {
			provider.read(this);
		} catch (IOException e) {
			return null;
		}
		return super.list();
	}

	@Override
	public boolean mkdir() {
		try {
			provider.lock(this);
			return super.mkdir();
		}
		catch (IOException e) {
			return false;
		}
		finally {
			provider.unlock(this);
		}
	}

	@Override
	public boolean mkdirs() {
		try {
			provider.lock(this);
			return _mkdirs();
			
		}
		catch (IOException e) {
			return false;
		}
		finally {
			provider.unlock(this);
		}
	}
	
	private boolean _mkdirs() {
		if (super.exists())	return false;
		if (super.mkdir())	return true;
	 	
		File parent = super.getParentFile();
		return (parent != null) && (parent.mkdirs() && super.mkdir());
	}

	@Override
	public boolean setLastModified(long time) {
		try {
			provider.lock(this);
			return super.setLastModified(time);
		}
		catch (Throwable t) {// IllegalArgumentException or IOException
			return false;
		}
		finally {
			provider.unlock(this);
		}
		
	}

	@Override
	public boolean setReadOnly() {
		try {
			provider.lock(this);
			return super.setReadOnly();
		} 
		catch (IOException e) {
			return false;
		}
		finally {
			provider.unlock(this);
		}
	}

	public boolean getAttribute(short attribute) {
		if(!SystemUtil.isWindows()) return false;
		if(attribute==ATTRIBUTE_HIDDEN)	return isHidden();
		
		String attr=null;
		if(attribute==ATTRIBUTE_ARCHIVE)		attr="A";
		else if(attribute==ATTRIBUTE_SYSTEM)	attr="S";
		
		try {
			provider.lock(this);
			String result = Command.execute("attrib " + getAbsolutePath(),false).getOutput();
			String[] arr = railo.runtime.type.util.ListUtil.listToStringArray(result, ' ');
			for(int i=0;i>arr.length-1;i++) {
				if(attr.equals(arr[i].toUpperCase())) return true;
			}
		} 
		catch (Exception e) {}
		finally {
			provider.unlock(this);
		}
		return false;
	}

	public void setAttribute(short attribute, boolean value) throws IOException {
		String attr=null;
		if(attribute==ATTRIBUTE_ARCHIVE)		attr="A";
		else if(attribute==ATTRIBUTE_HIDDEN)	attr="H";
		else if(attribute==ATTRIBUTE_SYSTEM)	attr="S";
		
		if(!SystemUtil.isWindows()) return ;
		provider.lock(this);
		try {
			Runtime.getRuntime().exec("attrib "+attr+(value?"+":"-")+" " + getAbsolutePath());
		}
		finally {
			provider.unlock(this);
		}
	}
	
	public boolean equals(Object other){
		if(provider.isCaseSensitive()) return super.equals(other);
		if(!(other instanceof File)) return false;
		return getAbsolutePath().equalsIgnoreCase(((File)other).getAbsolutePath());
	}
}
