{-# LANGUAGE DeriveAnyClass      #-}
{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-
Module: Projectile

This module provides utility functions to gather various paths of a project
-}
module Projectile (getProjectRootDir) where

import Protolude hiding (catch, (<>))

import Control.Exception.Safe (MonadCatch, MonadThrow, catch, throwM)
import Data.Monoid            ((<>))
import Data.Vector            (Vector)
import Path                   (Abs, Dir, Path, Rel, parent, parseRelFile, (</>))
import Path.IO                (isLocationOccupied)

import qualified Data.Vector as V

--------------------------------------------------------------------------------

data ProjectileException
  -- | Error thrown when calling project function from
  -- a path that does not not belong to any project
  = ProjectRootNotFound
  deriving (Generic, NFData, Show, Eq)

instance Exception ProjectileException

data WalkAction
  = WalkFinish
  | WalkContinue
  | WalkInvalid
  deriving (Generic, NFData, Show, Eq)

--------------------------------------------------------------------------------

isRoot :: Path Abs Dir -> Bool
isRoot path =
  parent path == path

-- | A list of files considered to mark the root of a project. The top-most
-- match has precedence.
projectRootTopLangMarkFiles :: Vector FilePath
projectRootTopLangMarkFiles =
  V.fromList
    [
      "rebar.config"       -- Rebar project file
    , "project.clj"        -- Leiningen project file
    , "build.boot"         -- Boot-clj project file
    , "SConstruct"         -- Scons project file
    , "pom.xml"            -- Maven project file
    , "build.sbt"          -- SBT project file
    , "gradlew"            -- Gradle wrapper script
    , "build.gradle"       -- Gradle project file
    , ".ensime"            -- Ensime configuration file
    , "Gemfile"            -- Bundler file
    , "requirements.txt"   -- Pip file
    , "setup.py"           -- Setuptools file
    , "tox.ini"            -- Tox file
    , "composer.json"      -- Composer project file
    , "Cargo.toml"         -- Cargo project file
    , "mix.exs"            -- Elixir mix project file
    , "stack.yaml"         -- Haskell's stack tool based project
    , "stack.yml"          -- Haskell's stack tool based project
    , "info.rkt"           -- Racket package description file
    , "DESCRIPTION"        -- R package description file
    , "TAGS"               -- etags/ctags are usually in the root of project
    , "GTAGS"              -- GNU Global tags
    ]

-- | A list of files considered to mark the root of a project. The top-most
-- match has precedence.
projectRootTopMarkFiles :: Vector FilePath
projectRootTopMarkFiles =
  V.fromList
    [
      ".projectile" -- projectile project marker
    , ".git"        -- Git VCS root dir
    , ".hg"         -- Mercurial VCS root dir
    , ".fslckout"   -- Fossil VCS root dir
    , "_FOSSIL_"    -- Fossil VCS root DB on Windows
    , ".bzr"        -- Bazaar VCS root dir
    , "_darcs"      -- Darcs VCS root dir
    ]

-- | A list of files considered to mark the root of a project. This
-- file must be in all sub-directories of a project.
projectRecurringMarkFiles :: Vector FilePath
projectRecurringMarkFiles =
  V.fromList
    [
      ".svn" -- Svn VCS root dir
    , "CVS"  -- Csv VCS root dir
    , "Makefile"
    ]

-- | Iterate from the current path to parent paths until the match function
-- returns a value for finishing the traversal
locateDominatingFile
  :: (MonadIO m, MonadThrow m)
  => Path Abs Dir                   -- ^ Directory to start from
  -> (Path Abs Dir -> m WalkAction) -- ^ Match function that will return what to do next on the iteration
  -> m (Path Abs Dir)               -- ^ The path where the match function returns @WalkFinish@
locateDominatingFile dir continueP
  | isRoot dir =
    throwM ProjectRootNotFound

  | otherwise = do
    walkNext <- continueP dir
    case walkNext of
      WalkInvalid ->
        throwM ProjectRootNotFound
      WalkFinish ->
        return dir
      WalkContinue ->
        locateDominatingFile (parent dir) continueP

-- | Returns a @WalkAction@ that indicates a finish of iteration when any of the
-- given relative paths is contained on the given directory
doesContainAny
  :: MonadIO m
  => Vector (Path Rel t) -- ^ Relative path that should be contained in directory input
  -> Path b Dir          -- ^ Directory path that should contain any of the relative paths
  -> m WalkAction
doesContainAny files dir = do
  matchesAnyFile <-
    (not . V.null . V.dropWhile not)
    <$> V.mapM (\file -> isLocationOccupied (dir </> file)) files

  if matchesAnyFile then
    return WalkFinish
  else
    return WalkContinue

getDirWithRootProjectFile
  :: (MonadIO m, MonadThrow m)
  => Path Abs Dir
  -> m (Path Abs Dir)
getDirWithRootProjectFile currentDir = do
  files <-
    mapM parseRelFile (projectRootTopMarkFiles
                       <> projectRootTopLangMarkFiles)
  locateDominatingFile currentDir (doesContainAny files)

getDirWithRecurringProjectFile
  :: (MonadIO m, MonadThrow m)
  => Path Abs Dir
  -> m (Path Abs Dir)
getDirWithRecurringProjectFile currentDir =
  let
    parentDoesNotContainOneOf files dir = do
      fileLocated <- doesContainAny files dir
      if fileLocated == WalkFinish then do
        -- return check of parent directory not containing one of the
        -- recurring project files
        parentContains <- doesContainAny files (parent dir)
        if parentContains == WalkFinish then
          return WalkContinue
        else
          return WalkFinish
      -- if the path doesn't contain the recurring marking file, then
      -- this is not a project that can be recognizable
      else
        return WalkInvalid

  in do
    files <- mapM parseRelFile projectRecurringMarkFiles
    locateDominatingFile currentDir (parentDoesNotContainOneOf files)

-- | Retrieves the root of the current project if available.
-- A @ProjectRootNotFound@ error is returned otherwise.
--
getProjectRootDir
  :: (MonadCatch m, MonadIO m)
  => Path Abs Dir      -- ^ Directory from where to look the root of the project
  -> m (Path Abs Dir)  -- ^ Root of the project directory
getProjectRootDir dir =
  catch (getDirWithRecurringProjectFile dir)
        (\(_ :: ProjectileException) -> getDirWithRootProjectFile dir)