package org.jboss.windup.graph.model.resource;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.EnumUtils;
import org.jboss.windup.graph.IndexType;
import org.jboss.windup.graph.Indexed;
import org.jboss.windup.graph.Indexes;
import org.jboss.windup.graph.frames.FrameBooleanDefaultValue;
import org.jboss.windup.graph.model.ArchiveModel;
import org.jboss.windup.graph.model.BelongsToProject;
import org.jboss.windup.graph.model.DuplicateProjectModel;
import org.jboss.windup.graph.model.ProjectModel;

import com.tinkerpop.blueprints.Direction;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.frames.Adjacency;
import com.tinkerpop.frames.Property;
import com.tinkerpop.frames.modules.javahandler.JavaHandler;
import com.tinkerpop.frames.modules.javahandler.JavaHandlerContext;
import com.tinkerpop.frames.modules.typedgraph.TypeValue;
import org.jboss.windup.graph.model.WindupVertexFrame;

/**
 * Represents a File on disk.
 */
@TypeValue(FileModel.TYPE)
public interface FileModel extends ResourceModel, BelongsToProject
{
    String TYPE = "FileResource";

    String PARENT_FILE = "parentFile";
    String SHA1_HASH = "sha1Hash";
    String MD5_HASH = "md5Hash";
    String FILE_NAME = "fileName";
    String FILE_PATH = "filePath";
    String IS_DIRECTORY = "isDirectory";
    String WINDUP_GENERATED = "windupGenerated";
    String PARSE_ERROR = "parseError";
    String ON_PARSE_ERROR = "onParseError";

    enum OnParseError {
        IGNORE, WARN;
        OnParseError fromName(String name){
            return EnumUtils.getEnum(OnParseError.class, StringUtils.upperCase(name));
        }
    }

    /**
     * Contains the File Name (the last component of the path). Eg, a file /tmp/foo/bar/file.txt would have fileName set to "file.txt"
     */
    @Property(FILE_NAME)
    String getFileName();

    /**
     * Contains the File Name (the last component of the path). Eg, a file /tmp/foo/bar/file.txt would have fileName set to "file.txt"
     */
    @Indexes({
        @Indexed,
        @Indexed(value=IndexType.SEARCH, name="filenamesearchindex")
    })
    @Property(FILE_NAME)
    void setFileName(String filename);

    /**
     * Contains the full path to the file (eg, /tmp/foo/bar/file.txt)
     */
    @Indexed
    @Property(FILE_PATH)
    String getFilePath();

    /**
     * Contains the full path to the file (eg, /tmp/foo/bar/file.txt)
     */
    // implemented via a handler that makes sure the isDirectory property is set as well
    @JavaHandler
    void setFilePath(String filePath);

    /**
     * Indicates whether the file is a directory or not
     */
    @Property(IS_DIRECTORY)
    boolean isDirectory();

    /**
     * Contains a MD5 Hash of the file
     */
    @Property(MD5_HASH)
    String getMD5Hash();

    /**
     * Contains a MD5 Hash of the file
     */
    @Property(MD5_HASH)
    void setMD5Hash(String md5Hash);

    /**
     * Contains a SHA1 Hash of the file
     */
    @Property(SHA1_HASH)
    @Indexed
    String getSHA1Hash();

    /**
     * Contains a SHA1 Hash of the file
     */
    @Property(SHA1_HASH)
    void setSHA1Hash(String sha1Hash);

    /**
     * Did Windup encounter problems when parsing this file? If so, this will contain a description of the error.
     */
    @Property(PARSE_ERROR)
    String getParseError();

    /**
     * Did Windup encounter problems when parsing this file? If so, this will contain a description of the error.
     */
    @Property(PARSE_ERROR)
    void setParseError(String message);

    /**
     * What to do with a parsing error?
     */
    @Property(ON_PARSE_ERROR)
    OnParseError getOnParseError();

    /**
     * What to do with a parsing error?
     */
    @Property(ON_PARSE_ERROR)
    void setOnParseError(OnParseError ignore);


    /**
     * Parent directory
     */
    @Adjacency(label = PARENT_FILE, direction = Direction.OUT)
    FileModel getParentFile();

    /**
     * Parent directory
     */
    @Adjacency(label = PARENT_FILE, direction = Direction.OUT)
    void setParentFile(FileModel parentFile);

    /**
     * Files contained within this directory
     */
    @Adjacency(label = PARENT_FILE, direction = Direction.IN)
    Iterable<FileModel> getFilesInDirectory();

    /**
     * Add a file to this directory
     */
    @Adjacency(label = PARENT_FILE, direction = Direction.IN)
    void addFileToDirectory(FileModel fileModel);

    /**
     * Gets the {@link ProjectModel} that this file is a part of
     */
    @Adjacency(label = ProjectModel.PROJECT_MODEL_TO_FILE, direction = Direction.IN)
    ProjectModel getProjectModel();

    /**
     * Gets a {@link File} object representing this file
     */
    @JavaHandler
    File asFile() throws RuntimeException;

    /**
     * Returns an open {@link InputStream} for reading from this file
     */
    @JavaHandler
    InputStream asInputStream() throws RuntimeException;

    /**
     * Returns the path of this file within the archive (including all subarchives, etc)
     */
    @JavaHandler
    String getPrettyPath();

    /**
     * Returns the {@link ArchiveModel} that contains this file. If this file is an {@link ArchiveModel} then it will
     * return itself.
     */
    @JavaHandler
    ArchiveModel getArchive();

    /**
     * Returns the path of this file within the parent project (format suitable for reporting)
     */
    @JavaHandler
    String getPrettyPathWithinProject();


    /**
     * Returns the application that this file is a part of. This is especially useful in the case of analyzing multiple application's, as we often
     * need to know which application a particular file is associated with.
     *
     * This is a shortcut for calling getProjectModel().getRootProjectModel().
     */
    @JavaHandler
    ProjectModel getApplication();

    /**
     * Specifies if the given file was generated by windup or it originates from application.
     */
    @Property(WINDUP_GENERATED)
    Boolean isWindupGenerated();

    /**
     * Specifies if the given file was generated by windup or it originates from application.
     */
    @FrameBooleanDefaultValue(false)
    @Property(WINDUP_GENERATED)
    void setWindupGenerated(boolean generated);

    @Override
    @JavaHandler
    boolean belongsToProject(ProjectModel projectModel);

    @Override
    @JavaHandler
    Iterable<ProjectModel> getRootProjectModels();

    abstract class Impl implements FileModel, JavaHandlerContext<Vertex>, BelongsToProject
    {
        public ProjectModel getApplication()
        {
            ProjectModel projectModel = getProjectModel();
            if (projectModel == null)
                return null;

            return projectModel.getRootProjectModel();
        }

        public String getPrettyPathWithinProject()
        {
            String result;
            ProjectModel projectModel = getProjectModel();
            if (projectModel == null)
            {
                // no project, just return the whole path
                result = getPrettyPath();
            }
            else if (projectModel.getRootFileModel().getFilePath().equals(getFilePath()))
            {
                result = "";
            }
            else
            {
                String filename = getFileName();
                if (getParentFile() == null)
                {
                    result = filename;
                }
                else
                {
                    String parentPrettyPath = getParentFile().getPrettyPathWithinProject();
                    result = StringUtils.isEmpty(parentPrettyPath) ? filename : parentPrettyPath + "/" + filename;
                }
            }
            return result;
        }

        public String getPrettyPath()
        {
            String filename = getFileName();
            String result;
            if (getParentFile() == null)
            {
                result = filename;
            }
            else
            {
                result = getParentFile().getPrettyPath() + "/" + filename;
            }
            return result;
        }

        public void setFilePath(String filePath)
        {
            File file = new File(filePath);
            // set the isDirectory attribute
            it().setProperty(IS_DIRECTORY, file.isDirectory());
            it().setProperty(FILE_PATH, file.getAbsolutePath());
            it().setProperty(FILE_NAME, file.getName());
        }

        @Override
        public ArchiveModel getArchive()
        {
            // required due to a quirk of the way frames works with @JavaHandler methods
            WindupVertexFrame reframed = frame(it(), WindupVertexFrame.class);
            if (reframed instanceof ArchiveModel)
            {
                return (ArchiveModel)reframed;
            }

            if (getParentFile() == null)
                return null;
            else
                return getParentFile().getArchive();
        }

        @Override
        public InputStream asInputStream() throws RuntimeException
        {
            try
            {
                if (this.getFilePath() == null)
                    return null;

                File file = new File(getFilePath());
                return new FileInputStream(file);
            }
            catch (Exception e)
            {
                throw new RuntimeException("Exception reading resource.", e);
            }
        }

        @Override
        public File asFile() throws RuntimeException
        {
            if (this.getFilePath() == null)
                return null;

            return new File(getFilePath());
        }

        @Override
        public boolean belongsToProject(ProjectModel projectModel)
        {
            return this.getProjectModel().equals(this.getCanonicalProjectModel(projectModel));
        }

        @Override
        public Iterable<ProjectModel> getRootProjectModels()
        {
            ProjectModel rootProjectModel = this.getProjectModel().getRootProjectModel();

            List<ProjectModel> projectModelList = new ArrayList<>();
            projectModelList.add(rootProjectModel);

            return projectModelList;
        }
    }
}
