/*
 * Copyright 2012 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Eclipse Public License version 1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */

package org.jboss.forge.resources;

import java.io.BufferedInputStream;
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.lang.annotation.Annotation;

import javax.enterprise.inject.spi.BeanManager;

import org.jboss.forge.project.ProjectModelException;
import org.jboss.forge.project.services.ResourceFactory;
import org.jboss.forge.resources.events.ResourceCreated;
import org.jboss.forge.resources.events.ResourceDeleted;
import org.jboss.forge.resources.events.ResourceModified;
import org.jboss.forge.resources.events.ResourceRenamed;
import org.jboss.forge.resources.events.TempResourceCreated;
import org.jboss.forge.shell.util.OSUtils;

/**
 * A standard, built-in resource for representing files on the filesystem.
 *
 * @author Mike Brock
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
public abstract class FileResource<T extends FileResource<?>> extends AbstractResource<File>
{
   protected boolean scratch;

   protected File file;
   protected File scratchFile;

   protected long lastModification;

   protected FileResource(final ResourceFactory factory, final File file)
   {
      super(factory, null);
      setFlag(ResourceFlag.File);

      if ((this.file = file) != null)
      {
         this.lastModification = file.lastModified();
      }
   }

   @Override
   public String getName()
   {
      return file.getName();
   }
   
   @Override
   public String getFullyQualifiedName()
   {
      return file.getAbsolutePath();
   }

   /**
    * Get the actual underlying file resource that this resource instance represents, whether existing or non-existing.
    *
    * @return An instance of {@link File}
    */
   @Override
   public File getUnderlyingResourceObject()
   {
      if (scratch)
      {
         return scratchFile;
      }
      else
      {
         return file;
      }
   }

   @Override
   public InputStream getResourceInputStream()
   {
      try
      {
         return new BufferedInputStream(new FileInputStream(file));
      }
      catch (FileNotFoundException e)
      {
         throw new ResourceException("cannot obtain stream to file: file does not exist: " + file.getAbsolutePath());
      }
   }

   /**
    * Get the parent of the current resource. Returns null if the current resource is the project root.
    *
    * @return An instance of the resource parent.
    */
   @Override
   public Resource<?> getParent()
   {
      return file.getParentFile() != null ? new DirectoryResource(resourceFactory, file.getParentFile()) : null;
   }

   @Override
   public Resource<?> getChild(final String name)
   {
      throw new ResourceException("[" + this.getClass().getSimpleName() + "] can have no children");
   }

   /**
    * Create a new {@link Resource} instance for the target file. The new {@link Resource} should be of the same type as
    * <b>this</b>.
    *
    * @param file The file to create the resource instance from.
    * @return A new resource.
    */
   @Override
   public abstract Resource<File> createFrom(File file);

   @Override
   public boolean exists()
   {
      return getUnderlyingResourceObject().exists();
   }

   /**
    * Return true if this {@link FileResource} exists and is actually a directory, otherwise return false;
    */
   public boolean isDirectory()
   {
      return file.isDirectory();
   }

   /**
    * Returns true if the underlying resource has been modified on the file system since it was initially loaded.
    *
    * @return boolean true if resource is changed.
    */
   public boolean isStale()
   {
      return lastModification != getUnderlyingResourceObject().lastModified();
   }

   /**
    * Re-read the last modified timestamp for this resource.
    */
   public void markUpToDate()
   {
      lastModification = getUnderlyingResourceObject().lastModified();
   }

   /**
    * Create a new single directory for this resource. This will not succeed if any parent directories needed for this
    * resource to exist are missing. You should consider using {@link #mkdirs()}
    */
   public boolean mkdir()
   {
      if (file.mkdir())
      {
         fireResourceCreated();
         return true;
      }
      return false;
   }

   /**
    * Create all directories required for this resource to exist.
    */
   public boolean mkdirs()
   {
      if (file.mkdirs())
      {
         fireResourceCreated();
         return true;
      }
      return false;
   }

   /**
    * Delete this file, non-recursively.
    */
   @Override
   public boolean delete()
   {
      return delete(false);
   }

   /**
    * Delete this {@link Resource}, and all child resources.
    */
   @Override
   public boolean delete(final boolean recursive)
   {
      if (recursive)
      {
         if (_deleteRecursive(file, true))
         {
            fireResourceDeleted();
            return true;
         }
         return false;
      }

      if ((file.listFiles() != null) && (file.listFiles().length != 0))
      {
         throw new RuntimeException("directory not empty");
      }

      if (OSUtils.isWindows())
      {
         System.gc(); // ensure no lingering handles that would prevent deletion
      }

      if (file.delete())
      {
         fireResourceDeleted();
         return true;
      }
      return false;
   }

   /**
    * Requests that the file or directory denoted by this resource be deleted when the virtual machine terminates.
    * <p>
    * Once deletion has been requested, it is not possible to cancel the request. This method should therefore be used
    * with care.
    */
   public void deleteOnExit()
   {
      file.deleteOnExit();
   }

   private static boolean _deleteRecursive(final File file, final boolean collect)
   {
      if (collect && OSUtils.isWindows())
      {
         System.gc(); // ensure no lingering handles that would prevent deletion
      }

      if (file == null)
      {
         return false;
      }

      File[] children = file.listFiles();
      if (children != null)
      {
         for (File sf : children)
         {
            if (sf.isDirectory())
            {
               _deleteRecursive(sf, false);
            }
            else
            {
               if (!sf.delete())
               {
                  throw new RuntimeException("failed to delete: " + sf.getAbsolutePath());
               }
            }
         }
      }

      return file.delete();
   }

   /**
    * Set the contents of this {@link FileResource} to the given {@link String}
    */
   public T setContents(String data)
   {
      if (data == null)
      {
         data = "";
      }
      return setContents(data.toCharArray());
   }

   /**
    * Set the contents of this {@link FileResource} to the given character array.
    */
   public T setContents(final char[] data)
   {
      return setContents(new ByteArrayInputStream(new String(data).getBytes()));
   }

   @SuppressWarnings("unchecked")
   /**
    * Set the contents of this {@link FileResource} to the contents of the given {@link InputStream}.
    */
   public T setContents(final InputStream data)
   {
      try
      {
         if (!exists())
         {
            mkdirs();
            delete();
            if (!createNewFile())
            {
               throw new IOException("Failed to create file: " + file);
            }
         }

         file.delete();

         OutputStream out = new FileOutputStream(file);
         try
         {
            byte buf[] = new byte[1024];
            int len;
            while ((len = data.read(buf)) > 0)
            {
               out.write(buf, 0, len);
            }
         }
         finally
         {
            if (data != null)
               data.close();

            out.flush();
            out.close();
            if (OSUtils.isWindows())
            {
               System.gc();
            }
         }

         fireResourceModified();
      }
      catch (IOException e)
      {
         throw new ProjectModelException(e);
      }
      return (T) this;
   }

   /**
    * Create the file in the underlying resource system. Necessary directory paths will be created automatically.
    */
   public boolean createNewFile()
   {
      try
      {
         if (file.mkdirs())
         {
            file.delete();
         }
         if (file.createNewFile())
         {
            fireResourceCreated();
            return true;
         }
         return false;
      }
      catch (IOException e)
      {
         throw new ProjectModelException(e);
      }
   }

   @SuppressWarnings("unchecked")
   /**
    * Create a temporary {@link FileResource}
    */
   public T createTempResource()
   {
      try
      {
         T result = (T) createFrom(File.createTempFile("forgetemp", ""));
         fireTempResourceCreated(result);
         return result;
      }
      catch (IOException e)
      {
         throw new ProjectModelException(e);
      }
   }

   private void fireTempResourceCreated(final T result)
   {
      resourceFactory.getManagerInstance().fireEvent(new TempResourceCreated(result), new Annotation[] {});
   }

   @Override
   @SuppressWarnings("unchecked")
   public <R extends Resource<?>> R reify(final Class<R> type)
   {
      Resource<?> queryResult = resourceFactory.getResourceFrom(file);
      if (type.isAssignableFrom(queryResult.getClass()))
      {
         return (R) queryResult;
      }
      else
      {
         return null;
      }
   }

   /**
    * Rename this resource to the given path.
    */
   public boolean renameTo(final String pathspec)
   {
      return renameTo(new File(pathspec));
   }

   /**
    * Rename this resource to the given {@link FileResource}
    */
   public boolean renameTo(final FileResource<?> target)
   {
      return renameTo(target.getUnderlyingResourceObject());
   }

   /**
    * Rename this resource to the given {@link File} name.
    */
   private boolean renameTo(final File target)
   {
      File original = file.getAbsoluteFile();
      if (file.renameTo(target))
      {
         fireResourceMoved(original);
         return true;
      }
      return false;
   }

   private void fireResourceMoved(final File original)
   {
      if (resourceFactory != null)
      {
         BeanManager manager = resourceFactory.getManagerInstance();
         if (manager != null)
         {
            manager.fireEvent(
                     new ResourceRenamed(this, original.getAbsolutePath(), file.getAbsolutePath()));
         }
      }
   }

   private void fireResourceModified()
   {
      if (resourceFactory != null)
      {
         BeanManager manager = resourceFactory.getManagerInstance();
         if (manager != null)
         {
            manager.fireEvent(new ResourceModified(this));
         }
      }
   }

   private void fireResourceCreated()
   {
      if (resourceFactory != null)
      {
         BeanManager manager = resourceFactory.getManagerInstance();
         if (manager != null)
         {
            manager.fireEvent(new ResourceCreated(this));
         }
      }
   }

   private void fireResourceDeleted()
   {
      if (resourceFactory != null)
      {
         BeanManager manager = resourceFactory.getManagerInstance();
         if (manager != null)
         {
            manager.fireEvent(new ResourceDeleted(this));
         }
      }
   }
}
