-- | Approximate a solution to 2D Euclidean TSP using the Lin-Kernighan
-- heuristic.
module Algorithms.Concorde.LinKern
    ( -- * The heuristic
      tsp, R2
      -- * Configuration
    , Config(..), defConfig
    ) where

import Control.Monad
import Control.Exception
import Data.Maybe
import System.Exit
import System.IO
import System.IO.Temp
import Text.Printf
import Safe

import qualified Data.IntMap    as IM
import qualified System.Process as P

errStr :: String -> String
errStr = ("Algorithms.Concorde.LinKern: " ++)

-- | Configuration for @'tsp'@.
data Config = Config
    {  -- | Path to the @linkern@ executable.  Searches @$PATH@ by default.
      executable :: FilePath
      -- | If set, write progress information to standard output.
    , verbose    :: Bool
      -- | Stop looking for better solutions after this many seconds.
    , timeBound  :: Maybe Double
      -- | Run this many optimization steps.  Default is the number of points.
    , steps      :: Maybe Int
      -- | Run this many separate optimizations.  Default is 1.
    , runs       :: Int
      -- | Other command-line arguments to the @linkern@ executable.
    , otherArgs  :: [String]
    } deriving (Eq, Ord, Read, Show)

-- | Default configuration.
defConfig :: Config
defConfig = Config
    { executable = "linkern"
    , verbose    = False
    , timeBound  = Nothing
    , steps      = Nothing
    , runs       = 1
    , otherArgs  = [] }

-- | A point in Euclidean two-dimensional space.
type R2 = (Double, Double)

-- | Approximate a solution to the two-dimensional Euclidean Traveling
-- Salesperson Problem, using the Lin-Kernighan heuristic.
--
-- Invokes Concorde's @linkern@ executable as an external process.
--
-- Note: @linkern@ uses Euclidean distance rounded to the nearest integer.
-- You may need to scale up coordinates in the function passed to @'tsp'@.
tsp
    :: Config     -- ^ Configuration.
    -> (a -> R2)  -- ^ Gives the rectangular coordinates of each point; see below.
    -> [a]        -- ^ List of points to visit.
    -> IO [a]     -- ^ Produces points permuted in tour order.
tsp cfg getCoord xs =
    -- Log to a temp file if not verbose.
    -- On Unix we could open /dev/null, but this is not portable.
    withSystemTempFile "log"    $ \_          logHdl    ->
    withSystemTempFile "coords" $ \coordsPath coordsHdl ->
    withSystemTempFile "tour"   $ \tourPath   tourHdl   -> do

        -- Output coordinates in TSPLIB format
        let pts = IM.fromList $ zip [0..] xs
        mapM_ (hPutStrLn coordsHdl)
            [ "TYPE:TSP"
            , "DIMENSION:" ++ show (IM.size pts)
            , "EDGE_WEIGHT_TYPE:EUC_2D"
            , "NODE_COORD_SECTION" ]
        forM_ (IM.toList pts) $ \(i,p) -> do
            let (x,y) = getCoord p
            hPrintf coordsHdl "%d %f %f\n" i x y
        hPutStrLn coordsHdl "EOF"
        hClose coordsHdl

        -- Invoke linkern
        let optArg flag fmt proj = case proj cfg of
                Nothing -> []
                Just x  -> [flag, printf fmt x]

            allArgs = concat [ ["-o", tourPath]
                             , ["-r", show (runs cfg)]
                             , optArg "-t" "%f" timeBound
                             , optArg "-R" "%d" steps
                             , otherArgs cfg
                             , [coordsPath] ]

            subOut  = if verbose cfg then P.Inherit else P.UseHandle logHdl
            procCfg = (P.proc (executable cfg) allArgs) { P.std_out = subOut }

        (Nothing, Nothing, Nothing, procHdl) <- P.createProcess procCfg
        ec <- P.waitForProcess procHdl
        case ec of
            ExitSuccess   -> return ()
            ExitFailure n -> throwIO . ErrorCall . errStr $
                ("process exited with code " ++ show n ++ extra) where
                    extra | n == 127  = "; program not installed or not in path?"
                          | otherwise = ""

        -- Skip the first line, then read the first int of each remaining
        -- line as an index into the original list of points.
        lns <- lines `fmap` hGetContents tourHdl
        _   <- evaluate (length lns)
        let get = headMay >=> readMay >=> flip IM.lookup pts
            fj  = fromMaybe (error (errStr "internal error in lookup"))
        return $ map (fj . get . words) (drop 1 lns)