module Linspire.Debian.SourcesList
    (DebSource(DebSource),
     SourceType(Deb, DebSrc),
     parseSourceLine,   -- String -> DebSource
     parseSourcesList,  -- String -> [DebSource]
     quoteWords,        -- String -> [String]
     archFiles)         -- FilePath -> Maybe String -> DebSource -> [FilePath]
    where

import Data.List
import Network.URI

{-

deb uri distribution [component1] [componenent2] [...]

The URI for the deb type must specify the base of the Debian
distribution, from which APT will find the information it needs.

distribution can specify an exact path, in which case the components
must be omitted and distribution must end with a slash (/).

If distribution does not specify an exact path, at least one component
must be present.

Distribution may also contain a variable, $(ARCH), which expands to
the Debian architecture (i386, m68k, powerpc, ...)  used on the
system.

The rest of the line can be marked as a comment by using a #.

Additional Notes:

 + Lines can begin with leading white space.

 + If the dist ends with slash (/), then it must be an absolute path
   and it is an error to specify components after it.

-}

data SourceType
    = Deb | DebSrc
    deriving Eq

instance Show SourceType where
    show Deb = "deb"
    show DebSrc = "deb-src"

data DebSource
    = DebSource
    { sourceType :: SourceType
    , sourceUri :: URI
    , sourceDist :: Either String (String, [String]) -- ^ Either (ExactPath) (Distribution, [Section])
    }
    deriving Eq

instance Show DebSource where
    show (DebSource thetype theuri thedist) =
        (show thetype) ++" "++ (uriToString id theuri " ") ++
        case thedist of
          Left exactPath -> escape exactPath
          Right (dist, sections) ->
              (escape dist) ++ " " ++ concat (intersperse " " (map escape sections))
        where escape = escapeURIString isAllowedInURI

-- |quoteWords - similar to words, but with special handling of
-- double-quotes and brackets.
--
-- The handling double quotes and [] is supposed to match:
-- apt-0.6.44.2\/apt-pkg\/contrib\/strutl.cc:ParseQuoteWord()
--
-- The behaviour can be defined as:
--
--  Break the string into space seperated words ignoring spaces that
--  appear between \"\" or []. Strip trailing and leading white space
--  around words. Strip out double quotes, but leave the square
--  brackets intact.
quoteWords :: String -> [String]
quoteWords [] = []
quoteWords s = quoteWords' Nothing (dropWhile (==' ') s)
    where
      quoteWords' :: Maybe Char -> String -> [String]
      quoteWords' Nothing [] = []
      quoteWords' Nothing str =
          case break (flip elem " [\"") str of
            ([],[]) -> []
            (w, []) -> [w]
            (w, (' ':rest)) -> w : (quoteWords' Nothing (dropWhile (==' ') rest))
            (w, ('"':rest)) ->
                case break (== '"') rest of
                  (w',('"':rst)) ->
                      case quoteWords' Nothing rst of
                        [] ->  [w ++ w']
                        (w'':ws) -> ((w ++ w' ++ w''): ws)
                  (_,[]) -> error ("quoteWords: missing \" in the string: "  ++ s)
            (w, ('[':rst)) ->
                case break (== ']') rst of
                  (w',(']':rsts)) ->
                      case quoteWords' Nothing rsts of
                        []       -> [w ++ "[" ++ w' ++ "]"]
                        (w'':ws) -> ((w ++ "[" ++ w' ++ "]" ++ w''): ws)
                  (_,[]) -> error ("quoteWords: missing ] in the string: "  ++ s)

stripLine :: String -> String
stripLine = takeWhile (/= '#') . dropWhile (== ' ')

sourceLines :: String -> [String]
sourceLines = filter (not . null) . map stripLine . lines

-- |parseSourceLine -- parses a source line
-- the argument must be a non-empty, valid source line with comments stripped
-- see: 'sourceLines'
parseSourceLine :: String -> DebSource
parseSourceLine str =
    case quoteWords str of
      (theTypeStr : theUriStr : theDistStr : sectionStrs) ->
          let theType = case unEscapeString theTypeStr of
                          "deb" -> Deb
                          "deb-src" -> DebSrc
                          o -> error ("parseSourceLine: invalid type " ++ o ++ " in line:\n" ++ str)
              theUri = case parseURI theUriStr of
                         Nothing -> error ("parseSourceLine: invalid uri " ++ theUriStr ++ " in the line:\n" ++ str)
                         Just u -> u
              theDist = unEscapeString theDistStr
          in
            case last theDist of
              '/' -> if null sectionStrs
                      then DebSource { sourceType = theType, sourceUri = theUri, sourceDist = Left theDist }
                      else error ("parseSourceLine: Dist is an exact path, so sections are not allowed on the line:\n" ++ str)
              _ -> if null sectionStrs
                    then error ("parseSourceLine: Dist is not an exact path, so at least one section is required on the line:\n" ++ str)
                    else DebSource { sourceType = theType, sourceUri = theUri, sourceDist = Right (theDist, map unEscapeString sectionStrs) }
      _ -> error ("parseSourceLine: invalid line in sources.list:\n" ++ str)

parseSourcesList :: String -> [DebSource]
parseSourcesList = map parseSourceLine . sourceLines

-- |Return the list of files that apt-get update would write into
-- \/var\/lib\/apt\/lists when it processed the given list of DebSource.
-- FIXME: remove the root argument from this and just return the names.
archFiles :: FilePath -> Maybe String -> DebSource -> [FilePath]
archFiles root (Just arch) deb@(DebSource Deb _ _) =
    map (++ ("_binary-" ++ arch ++ "_Packages")) (archFiles' root deb)
archFiles root Nothing deb@(DebSource DebSrc _ _) =
    map (++ "_source_Sources") (archFiles' root deb)
archFiles _ _ _ = []    -- error?

archFiles' :: FilePath -> DebSource -> [FilePath]
archFiles' root deb =
    let uri = sourceUri deb
        distro = sourceDist deb in
    let schme = uriScheme uri
        auth = uriAuthority uri
        pth = uriPath uri in
    let userpass = maybe "" uriUserInfo auth
        reg = maybeOfString $ maybe "" uriRegName auth
        port = maybe "" uriPort auth in
    let (user, pass) = break (== ':') userpass in
    let user' = maybeOfString user
        pass' = maybeOfString pass in
    let uriText = prefix schme user' pass' reg port pth in
    -- what about dist?
    either (\ exact -> [root ++ (escapeURIString (/= '@') ("/var/lib/apt/lists/" ++ uriText ++ escape exact))])
           (\ (dist, sections) ->
                map (\ section -> root ++ (escapeURIString (/= '@')
                                           ("/var/lib/apt/lists/" ++
                                            uriText ++
                                            "_dists_" ++ escape dist ++ "_" ++
                                            escape section)))
                    sections)
           distro
    where
      -- If user is given and password is not, the user name is
      -- added to the file name.  Otherwise it is not.  Really.
      prefix "http:" (Just user) Nothing (Just host) port pth =
          user ++ host ++ port ++ escape pth
      prefix "http:" _ _ (Just host) port pth =
          host ++ port ++ escape pth
      prefix "ftp:" _ _ (Just host) _ pth =
          host ++ escape pth
      prefix "file:" Nothing Nothing Nothing "" pth =
          escape pth
      prefix "ssh:" (Just user) Nothing (Just host) port pth =
          user ++ host ++ port ++ escape pth
      prefix "ssh" _ _ (Just host) port pth =
          host ++ port ++ escape pth
      prefix _ _ _ _ _ _ = error ("invalid DebSource: " ++ show deb)
      maybeOfString "" = Nothing
      maybeOfString s = Just s
      escape s = consperse "_" (wordsBy (== '/') s)

-- |The mighty consperse function
consperse :: [a] -> [[a]] -> [a]
consperse sep items = concat (intersperse sep items)

wordsBy :: Eq a => (a -> Bool) -> [a] -> [[a]]
wordsBy p s =
    case (break p s) of
      (t, []) -> [t]
      (h, t) -> h : wordsBy p (drop 1 t)