-- |
-- Module      :  Sound.HTagLib.Internal
-- Copyright   :  © 2015 Mark Karpov
-- License     :  BSD 3 clause
--
-- Maintainer  :  Mark Karpov <markkarpov@opmbx.org>
-- Stability   :  experimental
-- Portability :  portable
--
-- Low-level interaction with underlying C API. You don't want to use this,
-- see "Sound.HTagLib" instead.

{-# LANGUAGE CPP                      #-}
{-# LANGUAGE DeriveDataTypeable       #-}
{-# LANGUAGE EmptyDataDecls           #-}
{-# LANGUAGE ForeignFunctionInterface #-}

module Sound.HTagLib.Internal
  ( -- * Data types
    FileId
  , FileType (..)
  , ID3v2Encoding (..)
  , HTagLibException (..)
    -- * File API
  , withFile
  , saveFile
    -- * Tag API
  , getTitle
  , getArtist
  , getAlbum
  , getComment
  , getGenre
  , getYear
  , getTrackNumber
  , setTitle
  , setArtist
  , setAlbum
  , setComment
  , setGenre
  , setYear
  , setTrackNumber
    -- * Audio properties API
  , getDuration
  , getBitRate
  , getSampleRate
  , getChannels
    -- * Special convenience ID3v2 functions
  , id3v2SetEncoding )
where

import Control.Exception
import Control.Monad (when, unless)
import Data.Maybe (fromJust)
import Data.Typeable (Typeable)
import Foreign
import Foreign.C.String
import Foreign.C.Types

import qualified Sound.HTagLib.Type as T

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif

data TagLibFile
data TagLibTag
data TagLibProperties

-- | This is an abstraction representing opened file. Other modules can
-- pass it around and treat it like a black box.

newtype FileId = FileId (Ptr TagLibFile)

-- | Types of files TagLib can work with. This may be used to explicitly
-- specify type of file rather than relying on TagLib ability to guess type
-- of file from its extension.

data FileType
  = MPEG
  | OggVorbis
  | FLAC
  | MPC
  | OggFlac
  | WavPack
  | Speex
  | TrueAudio
  | MP4
  | ASF
    deriving (Show, Eq, Enum)

-- | Encoding for ID3v2 frames that are written to tags.

data ID3v2Encoding
  = ID3v2Latin1
  | ID3v2UTF16
  | ID3v2UTF16BE
  | ID3v2UTF8
    deriving (Show, Eq, Enum)

-- | The data type represents exceptions specific to the library. The
-- following constructors are defined:
--
--     * @OpeningFailed@ means that attempt to open audio file to read its
--     tags failed.
--     * @InvalidFile@ means that file can be opened, but it doesn't contain
--     any information that can be interpreted by the library.
--     * @SavingFailed@ is thrown when well… saving failed.

data HTagLibException
  = OpeningFailed FilePath
  | InvalidFile   FilePath
  | SavingFailed  FilePath
    deriving (Eq, Show, Typeable)

instance Exception HTagLibException

-- Misc

foreign import ccall unsafe "taglib/tag_c.h taglib_set_string_management_enabled"
  c_taglib_set_string_management_enabled :: CInt -> IO ()

-- File API

foreign import ccall unsafe "taglib/tag_c.h taglib_file_new"
  c_taglib_file_new :: CString -> IO (Ptr TagLibFile)

foreign import ccall unsafe "taglib/tag_c.h taglib_file_new_type"
  c_taglib_file_new_type :: CString -> CInt -> IO (Ptr TagLibFile)

foreign import ccall unsafe "taglib/tag_c.h taglib_file_free"
  c_taglib_file_free :: Ptr TagLibFile -> IO ()

foreign import ccall unsafe "taglib/tag_c.h taglib_file_is_valid"
  c_taglib_file_is_valid :: Ptr TagLibFile -> IO CInt

foreign import ccall unsafe "taglib/tag_c.h taglib_file_tag"
  c_taglib_file_tag :: Ptr TagLibFile -> IO (Ptr TagLibTag)

foreign import ccall unsafe "taglib/tag_c.h taglib_file_audioproperties"
  c_taglib_file_properties :: Ptr TagLibFile -> IO (Ptr TagLibProperties)

foreign import ccall unsafe "taglib/tag_c.h taglib_file_save"
  c_taglib_file_save :: Ptr TagLibFile -> IO CInt

-- Tag API

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_title"
  c_taglib_tag_title :: Ptr TagLibTag -> IO CString

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_artist"
  c_taglib_tag_artist :: Ptr TagLibTag -> IO CString

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_album"
  c_taglib_tag_album :: Ptr TagLibTag -> IO CString

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_comment"
  c_taglib_tag_comment :: Ptr TagLibTag -> IO CString

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_genre"
  c_taglib_tag_genre :: Ptr TagLibTag -> IO CString

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_year"
  c_taglib_tag_year :: Ptr TagLibTag -> IO CUInt

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_track"
  c_taglib_tag_track :: Ptr TagLibTag -> IO CUInt

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_set_title"
  c_taglib_tag_set_title :: Ptr TagLibTag -> CString -> IO ()

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_set_artist"
  c_taglib_tag_set_artist :: Ptr TagLibTag -> CString -> IO ()

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_set_album"
  c_taglib_tag_set_album :: Ptr TagLibTag -> CString -> IO ()

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_set_comment"
  c_taglib_tag_set_comment :: Ptr TagLibTag -> CString -> IO ()

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_set_genre"
  c_taglib_tag_set_genre :: Ptr TagLibTag -> CString -> IO ()

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_set_year"
  c_taglib_tag_set_year :: Ptr TagLibTag -> CUInt -> IO ()

foreign import ccall unsafe "taglib/tag_c.h taglib_tag_set_track"
  c_taglib_tag_set_track :: Ptr TagLibTag -> CUInt -> IO ()

-- Audio properties API

foreign import ccall unsafe "taglib/tag_c.h taglib_audioproperties_length"
  c_taglib_properties_length :: Ptr TagLibProperties -> IO CInt

foreign import ccall unsafe "taglib/tag_c.h taglib_audioproperties_bitrate"
  c_taglib_properties_bitrate :: Ptr TagLibProperties -> IO CInt

foreign import ccall unsafe "taglib/tag_c.h taglib_audioproperties_samplerate"
  c_taglib_properties_samplerate :: Ptr TagLibProperties -> IO CInt

foreign import ccall unsafe "taglib/tag_c.h taglib_audioproperties_channels"
  c_taglib_properties_channels :: Ptr TagLibProperties -> IO CInt

-- Special convenience ID3v2 functions

foreign import ccall unsafe "taglib/tag_c.h taglib_id3v2_set_default_text_encoding"
  c_taglib_id3v2_set_default_text_encoding :: CInt -> IO ()

-- Wrappers. Here we prepare a little higher-level interface that will be
-- used by the rest of the library.

-- File API

-- | Open audio file and return its ID (abstraction that rest of library can
-- pass around). In case of trouble 'IOException' is thrown.

newFile :: FilePath       -- ^ Path to audio file
        -> Maybe FileType -- ^ Type of file (or it will be guessed)
        -> IO FileId      -- ^ Id to pass around
newFile path ftype = do
  c_taglib_set_string_management_enabled $ fromBool False
  ptr <- withCString path $ \cstr ->
           case ftype of
             Nothing -> c_taglib_file_new cstr
             Just t  -> c_taglib_file_new_type cstr (enumToCInt t)
  when (ptr == nullPtr) $ throw (OpeningFailed path)
  valid <- toBool <$> c_taglib_file_is_valid ptr
  unless valid $ throw (InvalidFile path)
  return $ FileId ptr

-- | Free file given its ID. Every time you open a file, free it.

freeFile :: FileId -> IO ()
freeFile (FileId ptr) = c_taglib_file_free ptr

-- | Open audio file located at specified path, execute some actions given
-- its 'FileId' and then free the file.

withFile :: FilePath         -- ^ Path to audio file
         -> Maybe FileType   -- ^ Type of file (or it will be guessed)
         -> (FileId -> IO a) -- ^ Computation depending of 'FileId'
         -> IO a             -- ^ Result value
withFile path t = bracket (newFile path t) freeFile

-- | Save file given its ID. Given 'FilePath' just tells what to put into
-- exception if the action fails, it doesn't specify where to save the file
-- (it's determined by 'FileId').

saveFile :: FilePath -- ^ File name to use in exceptions
         -> FileId   -- ^ File identifier
         -> IO ()
saveFile path (FileId ptr) = do
  success <- toBool <$> c_taglib_file_save ptr
  unless success $ throw (SavingFailed path)

-- Tag API

-- | Get title tag associated with file.

getTitle :: FileId -> IO T.Title
getTitle = fmap T.mkTitle . getStrValue c_taglib_tag_title

-- | Get artist tag associated with file.

getArtist :: FileId -> IO T.Artist
getArtist = fmap T.mkArtist . getStrValue c_taglib_tag_artist

-- | Get album tag associated with file.

getAlbum :: FileId -> IO T.Album
getAlbum = fmap T.mkAlbum . getStrValue c_taglib_tag_album

-- | Get comment tag associated with file.

getComment :: FileId -> IO T.Comment
getComment = fmap T.mkComment . getStrValue c_taglib_tag_comment

-- | Get genre tag associated with file.

getGenre :: FileId -> IO T.Genre
getGenre = fmap T.mkGenre . getStrValue c_taglib_tag_genre

-- | Get year tag associated with file.

getYear :: FileId -> IO (Maybe T.Year)
getYear = fmap T.mkYear . getIntValue c_taglib_tag_year

-- | Get track number associated with file.

getTrackNumber :: FileId -> IO (Maybe T.TrackNumber)
getTrackNumber = fmap T.mkTrackNumber . getIntValue c_taglib_tag_track

-- | Set title of track associated with file.

setTitle :: T.Title -> FileId -> IO ()
setTitle v = setStrValue c_taglib_tag_set_title (T.getTitle v)

-- | Set artist of track associated with file.

setArtist :: T.Artist -> FileId -> IO ()
setArtist v = setStrValue c_taglib_tag_set_artist (T.getArtist v)

-- | Set album of track associated with file.

setAlbum :: T.Album -> FileId -> IO ()
setAlbum v = setStrValue c_taglib_tag_set_album (T.getAlbum v)

-- | Set comment of track associated with file.

setComment :: T.Comment -> FileId -> IO ()
setComment v = setStrValue c_taglib_tag_set_comment (T.getComment v)

-- | Set genre of track associated with file.

setGenre :: T.Genre -> FileId -> IO ()
setGenre v = setStrValue c_taglib_tag_set_genre (T.getGenre v)

-- | Set year of track associated with file.

setYear :: Maybe T.Year -> FileId -> IO ()
setYear v = setIntValue c_taglib_tag_set_year (T.getYear <$> v)

-- | Set track number of track associated with file.

setTrackNumber :: Maybe T.TrackNumber -> FileId -> IO ()
setTrackNumber v = setIntValue c_taglib_tag_set_track (T.getTrackNumber <$> v)

-- Audio properties API

-- | Get duration of track associated with file.

getDuration :: FileId -> IO T.Duration
getDuration = fmap (fromJust . T.mkDuration)
            . getIntProperty c_taglib_properties_length

-- | Get bit rate of track associated with file.

getBitRate :: FileId -> IO T.BitRate
getBitRate = fmap (fromJust . T.mkBitRate)
           . getIntProperty c_taglib_properties_bitrate

-- | Get sample rate of track associated with file.

getSampleRate :: FileId -> IO T.SampleRate
getSampleRate = fmap (fromJust . T.mkSampleRate)
              . getIntProperty c_taglib_properties_samplerate

-- | Get number of channels in track associated with file.

getChannels :: FileId -> IO T.Channels
getChannels = fmap (fromJust . T.mkChannels)
            . getIntProperty c_taglib_properties_channels

-- Special convenience ID3v2 functions

-- | Set the default encoding for ID3v2 frames that are written to tags.

id3v2SetEncoding :: ID3v2Encoding -> IO ()
id3v2SetEncoding = c_taglib_id3v2_set_default_text_encoding . enumToCInt

-- Helpers

getStrValue
  :: (Ptr TagLibTag -> IO CString) -- ^ How to get string from the resource
  -> FileId                        -- ^ File ID
  -> IO String                     -- ^ String result
getStrValue getStr (FileId ptr) = do
  tag    <- c_taglib_file_tag ptr
  cstr   <- getStr tag
  result <- peekCString cstr
  free cstr
  return result

getIntValue
  :: Integral a
  => (Ptr TagLibTag -> IO a) -- ^ How to get value from the resource
  -> FileId                  -- ^ File ID
  -> IO Int                  -- ^ Result value
getIntValue getInt (FileId ptr) = do
  tag  <- c_taglib_file_tag ptr
  cint <- getInt tag
  return $ fromIntegral cint

setStrValue
  :: (Ptr TagLibTag -> CString -> IO ()) -- ^ Setting routine
  -> String                              -- ^ New string value
  -> FileId                              -- ^ File ID
  -> IO ()
setStrValue setStr str (FileId ptr) = do
  tag <- c_taglib_file_tag ptr
  withCString str $ \cstr ->
    setStr tag cstr

setIntValue
  :: Integral a
  => (Ptr TagLibTag -> a -> IO ()) -- ^ Setting routine
  -> Maybe Int                     -- ^ New value
  -> FileId                        -- ^ File ID
  -> IO ()
setIntValue setUInt int (FileId ptr) = do
  tag <- c_taglib_file_tag ptr
  setUInt tag $ maybe 0 fromIntegral int

getIntProperty
  :: Integral a
  => (Ptr TagLibProperties -> IO a) -- ^ How to get value from the resource
  -> FileId                         -- ^ File ID
  -> IO Int                         -- ^ Result
getIntProperty getInt (FileId ptr) = do
  properties <- c_taglib_file_properties ptr
  value      <- getInt properties
  return $ fromIntegral value

-- | Convert Haskell enumeration to C enumeration (an integer).

enumToCInt :: Enum a => a -> CInt
enumToCInt = fromIntegral . fromEnum