packages feed

setdown-0.1.3.0: src/Main.hs

{-# LANGUAGE CPP                #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Main where

import qualified Data.ByteString.Lazy   as B
import qualified Data.Set               as S

import qualified Data.Text.Lazy         as LT
import qualified Data.Text.Lazy.IO      as T
import qualified Text.Layout.Table      as Tab
import           System.Console.CmdArgs
import           System.Exit

import           Control.Monad          (filterM, forM, forM_, unless, when)
import           Data.Word              (Word8)

import           Data.List              (intersperse, isSuffixOf, partition)
import           Data.Maybe             (fromMaybe)

import           Context
import           DefinitionHelpers
import           ExpressionConversion
import           ExternalSort
import           PrintDefinition
import           SetData
import           SetInput
import           SetInputVerification

import           DuplicateElimination
import           PerformOperations
import           SimpleDefinitionCycles

import           System.Directory       (doesFileExist, getCurrentDirectory,
                                         listDirectory, removeFile)
import           System.FilePath        (dropFileName, (</>))

#ifndef mingw32_HOST_OS
import           System.Posix.Files     (createLink)
#endif

-- Useful for Print Debugging
-- import Text.Show.Pretty
-- prettyPrint :: Show a => a -> IO ()
-- prettyPrint = putStrLn . ppShow


data Options = Options
   { outputDirectory :: Maybe FilePath
   , setdownFile     :: Maybe FilePath
   , showTransient   :: Bool
   } deriving (Show, Data, Typeable)

options :: Options
options = Options
   { outputDirectory = def
      &= explicit
      &= name "output"
      &= name "o"
      &= typDir
      &= help "Directory in which to place output files, relative to your .setdown file. Defaults to 'output' if omitted."
      &= opt "output"
   , setdownFile = def
      &= name "input"
      &= name "i"
      &= explicit
      &= help "The .setdown definitions file to evaluate. If omitted, setdown looks for a single .setdown file in the current directory and uses it automatically. Exits with an error if zero or more than one are found."
      &= typ "definitions.setdown"
   , showTransient = def
      &= explicit
      &= name "show-transient"
      &= help "Also show intermediate results for sub-expressions generated internally to evaluate your definitions. Useful for debugging complex .setdown files."
   }
   &= program "setdown"
   &= summary "setdown evaluates a .setdown definitions file to perform set operations (intersection, union, difference) on line-based text files, writing one result file per definition to an output directory."

data GuessError
  = NoMatchingFiles
  | TooManyMatchingFiles [FilePath]

-- attempt to use the
guessInputFile :: IO (Either GuessError FilePath)
guessInputFile = do
  currentDirFiles <- listDirectory =<< getCurrentDirectory
  case filter hasSetdownExtension currentDirFiles of
    [] -> return . Left $ NoMatchingFiles
    [x] -> return . Right $ x
    xs -> return . Left $ TooManyMatchingFiles xs
  where
    hasSetdownExtension filePath = ".setdown" `isSuffixOf` filePath

getInputFileOrFail :: Maybe FilePath -> IO FilePath
getInputFileOrFail (Just userSuggested) = do
  inputFileExists <- doesFileExist userSuggested
  if inputFileExists
    then return userSuggested
    else do
      putStrLn $ "Error: The given setdown file did not exist: " ++ userSuggested
      exitWith (ExitFailure 1)
getInputFileOrFail Nothing = do
  guessResult <- guessInputFile
  case guessResult of
    (Right match) -> do
      inputFileExists <- doesFileExist match
      if inputFileExists
        then return match
        else do
         putStrLn $ "Error: The given setdown file did not exist: " ++ match
         exitWith (ExitFailure 1)
    (Left (TooManyMatchingFiles matches)) -> do
      putStrLn $ "Error: There were too many files that look like setdown definition files in the current directory. Could not pick one. Select one with '--input'."
      putStrLn $ "Matching files: " ++ (show matches)
      exitWith (ExitFailure 2)
    (Left NoMatchingFiles) -> do
      putStrLn $ "Error: There were no files that look like setdown definition files in the current directory. Write one or select one with '--input'."
      exitWith (ExitFailure 3)

-- TODO the setdownFile should be optional, at which point we should search the current directory
-- for one
main :: IO ()
main = do
   opts <- cmdArgs options

   inputFilePath <- getInputFileOrFail (setdownFile opts)
   putStrLn $ "==> Using setdown file: " ++ inputFilePath
   printNewline

   -- Todo work out the parent directory of the setdown file
   putStrLn "==> Creating the environment..."
   let baseDir = dropFileName inputFilePath

   let context = standardContext
                  { cBaseDir = baseDir
                  , cOutputDir = baseDir </> fromMaybe "output" (outputDirectory opts)
                  }

   putStrLn $ "  Base Directory: " ++ cBaseDir context
   putStrLn $ "  Output Directory: " ++ cOutputDir context
   prepareContext context
   printNewline

   setData <- parse <$> (B.readFile inputFilePath)
   putStrLn "==> Parsed original definitions..."
   printDefinitions setData
   printNewline

   -- Step 0: Verify that the definitions are well defined and that the referenced files exist
   -- relative to the file that we pass in.
   putStrLn "==> Verification (Ensuring correctness in the set definitions file)"
   case duplicateDefinitionName setData of
      [] -> putStrLn "  OK: No duplicate definitions found."
      xs -> do
         putStrLn "[Error 11] Duplicate definitions found:"
         mapM_ T.putStrLn xs
         exitWith (ExitFailure 11)

   case unknownIdentifier setData of
      [] -> putStrLn "  OK: No unknown identifiers found."
      xs -> do
         putStrLn "[Error 12] Unknown identifiers used in the set descriptor file:"
         mapM_ T.putStrLn xs
         exitWith (ExitFailure 12)

   allFiles <- filesNotFound context . S.toList . extractFilenamesFromDefinitions $ setData
   unless (null allFiles) $ do
      putStrLn "[Error 13] the following files could not be found:"
      forM_ allFiles (\fp -> putStrLn $ " - " ++ fp)
      exitWith (ExitFailure 13)
   putStrLn "  OK: All files in the definitions could be found."
   printNewline

   putStr "==> Simplifying and eliminating duplicates from set definitions..."
   let simpleSetData = eliminateDuplicates . orderDefinitions . complexToSimpleDefinitions $ setData
   if showTransient opts
      then do
         putStrLn "DONE:"
         printSimpleDefinitions simpleSetData
         printNewline
      else do
         putStrLn "DONE!"
         printNewline

   putStr "==> Checking for cycles in the simplified definitions..."
   let cycles = getCyclesInSimpleDefinitions simpleSetData
   putStrLn "DONE:"
   unless (null cycles) $ do
      putStrLn "[Error 20] found cyclic dependencies in the definitions!"
      printNewline
      putStrLn "We found the following cycles:"
      printCycles cycles
      exitWith (ExitFailure 20)
   putStrLn "  OK: No cycles were found in the definitions."
   printNewline

   putStrLn "==> Sorting and de-duplicated input files from the definitions..."
   -- Step 1: For every unique file, sort it (Use external sort for this purpose:
   -- https://hackage.haskell.org/package/external-sort-0.2/docs/Algorithms-ExternalSort.html add
   -- docs to that library if at all possible)
   -- TODO use file timestamps to not sort these big files more than once if possible
   sortedFiles <- extractAndSortFiles context (S.toList . extractFilenamesFromDefinitions $ setData) -- TODO use the simple set data here
   printTabularResults sortedFiles
   printNewline

   putStrLn "==> Setdown results"
   -- Step 2: Calculate the graph of everything that needs to be computed and compute things one at
   -- a time. Even make sure that you store the temporary results along the way. That way we can
   -- refer to them later if the same computation is made twice. We should certainly memoize with
   -- the file system. It would be great if we could print out the results of the computations as we
   -- go.
   computedFiles <- runSimpleDefinitions context simpleSetData sortedFiles
   -- Step 3: Publish retained results under their definition names.
   publishedFiles <- publishResults context computedFiles
   -- Step 4: Count the elements in each result file.
   annotatedFiles <- forM publishedFiles $ \(sd, fp) -> do
      n <- countLines fp
      return (sd, fp, n)
   -- Step 5: Print out the final statistics with the definitions pointing to how many elements that
   -- each contained and where to find their output files.
   printComputedResults opts annotatedFiles

publishResults :: Context -> [(SimpleDefinition, FilePath)] -> IO [(SimpleDefinition, FilePath)]
#ifndef mingw32_HOST_OS
publishResults ctx results = mapM publish results
  where
   publish (sd, src)
      | sdRetain sd = do
            let dest = cOutputDir ctx </> LT.unpack (sdId sd) ++ ".txt"
            destExists <- doesFileExist dest
            when destExists $ removeFile dest
            createLink src dest
            return (sd, dest)
      | otherwise   = return (sd, src)
#else
publishResults _ results = return results
#endif

countLines :: FilePath -> IO Int
countLines fp = fromIntegral . B.count newlineByte <$> B.readFile fp
   where newlineByte = 0x0A :: Word8

filesNotFound :: Context -> [FilePath] -> IO [FilePath]
filesNotFound ctx = filterM (\x -> not <$> doesFileExist (cBaseDir ctx </> x))

printCycles :: [SimpleDefinitions] -> IO ()
printCycles sds = forM_ sds $ \sd -> do
   putStr "   "
   printCycle sd
   printNewline

printCycle :: SimpleDefinitions -> IO ()
printCycle [] = putStrLn "Not a cycle."
printCycle (x:xs) = sequence_ . intersperse (putStr " -> ") $ printIdentifiers
   where
      printIdentifiers = fmap (printIdentifier . sdId) loopRound
      loopRound = [x] ++ xs ++ [x]

-- TODO use the box library to print these items in a nice tabulated way
printSortResults :: [(FilePath, FilePath)] -> IO ()
printSortResults = sequence_ . fmap printSortResult

printSortResult :: (FilePath, FilePath) -> IO ()
printSortResult (unsortedFile, sortedFile) = do
   putStr . wrapInQuotes $ unsortedFile
   putStr " (unsorted) => "
   putStr . wrapInQuotes $ sortedFile
   putStrLn " (sorted)"
   where
      wrapInQuotes x = "\"" ++ x ++ "\""

printTabularResults :: [(FilePath, FilePath)] -> IO ()
printTabularResults fileMapping = sequence_ . fmap putStrLn $ Tab.tableLines (Tab.columnHeaderTableS columns Tab.unicodeBoldHeaderS headers rows)
   where
      headers = Tab.titlesH ["From", "To"]

      columns =
         [ Tab.column Tab.expand Tab.left Tab.noAlign Tab.noCutMark
         , Tab.column Tab.expand Tab.left Tab.noAlign Tab.noCutMark
         ]

      rows = [Tab.rowsG $ fmap (\(from, to) -> [from, to]) fileMapping]

printTabularResultsWithCount :: [(String, FilePath, Int)] -> IO ()
printTabularResultsWithCount rows = sequence_ . fmap putStrLn $ Tab.tableLines (Tab.columnHeaderTableS columns Tab.unicodeBoldHeaderS headers tableRows)
   where
      headers = Tab.titlesH ["Name", "File", "Count"]

      columns =
         [ Tab.column Tab.expand Tab.left  Tab.noAlign Tab.noCutMark
         , Tab.column Tab.expand Tab.left  Tab.noAlign Tab.noCutMark
         , Tab.column Tab.expand Tab.right Tab.noAlign Tab.noCutMark
         ]

      tableRows = [Tab.rowsG $ fmap (\(defName, fp, n) -> [defName, fp, show n]) rows]

printComputedResults :: Options -> [(SimpleDefinition, FilePath, Int)] -> IO ()
printComputedResults opts results = do
   unless (null tempResults || not (showTransient opts)) $ do
      putStrLn "Transient results:"
      printTabularResultsWithCount . fmap toRow $ tempResults
      printNewline
   unless (null retainResults) $ do
      unless (not (showTransient opts)) $ putStrLn "Required results:"
      printTabularResultsWithCount . fmap toRow $ retainResults
   where
      (retainResults, tempResults) = partition (\(sd, _, _) -> sdRetain sd) results
      getIdentifier (SimpleDefinition ident _ _) = ident
      toRow (sd, fp, n) = (LT.unpack (getIdentifier sd), fp, n)


printComputedResult :: (SimpleDefinition, FilePath) -> IO ()
printComputedResult (SimpleDefinition ident _ _, fp) = do
   printIdentifier ident
   putStr ": "
   putStrLn fp

printIdentifier :: Identifier -> IO ()
printIdentifier = T.putStr

printNewline :: IO ()
printNewline = putStrLn ""