#  Copyright (c) 2011, Patrick C. McGinty
#
#  This program is free software: you can redistribute it and/or modify it
#  under the terms of the Simplified BSD License.
#
#  See LICENSE text for more details.
"""
   mktoc.cmdline
   ~~~~~~~~~~~~~

   Command-line interface for Mktoc.
"""

import codecs
import chardet.universaldetector
import logging
import os
import re
import sys
import textwrap
import traceback
from optparse import OptionParser

from .base import *
from .parser import *


# WAV file reading command-line switch
#  - allow 'file not found' errors when reading WAV files
_OPT_ALLOW_WAV_FNF   = '-a'
# Offset correction command-line switch
# - enable output of offset corrected WAV files
_OPT_OFFSET_CORRECT  = '-c'
# Output CUE file name
_OPT_CUE_FILE        = '-f'
# Temp WAV Files
# - write offset corrected WAV files to /tmp dir
_OPT_TEMP_WAV        = '-t'
# WAV File List
# - create a TOC file using a list of WAV files
_OPT_WAV_LIST        = '-w'
# Mulitsession TOC
# - required when writting a multi-session TOC
_OPT_MULTI_SESSION  = '-m'
# - disable multi-session features, don't prompt
_OPT_IGNORE_MULTI_SESSION = '-z'


class CommandLine(object):
   """
   Command line runner class for executing Mktoc.

   Interprets all program arguments and creates a CueParser object to
   generate a final TOC file.
   """
   def run(self,argv=sys.argv[1:]):
      """Execution entry point."""
      try:
         self._run(argv)
      except TooManyFilesMatchError as e:
         self._error_msg_multi_files(e)
      except FileNotFoundError as e:
         self._error_msg_no_file(e)
      except MkTocError, e:
         self._error_msg(e)

   def _run(self,argv):
      # parse all command line arguments, exit if there is any error
      opt,args = self._parse_args(argv)
      # setup logging
      if opt.debug: logging.basicConfig(level=logging.DEBUG)
      # check if using WAV list or CUE file
      if opt.wav_files is None:
         # open CUE file
         if opt.cue_file:
            fh_in = self._open_file( opt.cue_file )
            # set the working dir of the input file
            cue_dir = os.path.dirname( fh_in.name ) or os.curdir
         else:
            cue_dir = os.curdir
            fh_in = sys.stdin
         # create CUE file parser
         assert(cue_dir)
         p = CueParser( cue_dir, opt.find_wav)
         cd_obj = p.parse( fh_in)
         fh_in.close()
      else:
         wav_dir = os.path.dirname( opt.wav_files[0] ) or os.curdir
         # create WAV list parser
         assert( wav_dir)
         p = WavParser( wav_dir, opt.find_wav)
         cd_obj = p.parse( opt.wav_files)
      # warn user when TOC is multi-session
      self._check_multisession_opt( cd_obj, opt)
      if opt.wav_offset:
         cd_obj.modWavOffset( opt.wav_offset, opt.write_tmp )
      toc = cd_obj.getToc()
      # open TOC file
      if opt.toc_file:
         fh_out = self._open_file( opt.toc_file,'wb')
         fh_out.write( self._banner_msg())
         for l in toc:
            fh_out.write("%s\n" % l)
      else:
         fh_out = sys.stdout
         fh_out.write( self._banner_msg())
         for l in toc:
            # assume utf-8 encoding to sysout
            fh_out.write(unicode("%s\n" % l).encode('utf-8'))
      fh_out.close()

      if cd_obj.disc.is_multisession:
         # print multi-session instructions; data session size is calulated by
         # frame length minus 2 frames. I'm not actually sure why 2 frames must
         # be subtracked, but it was verify to be correct. If your system/drive
         # behaves differntly, please file a bug report.
         print >> sys.stderr, textwrap.dedent("""
         #########################################################
         # Multi-Session TOC Mode
         #########################################################

         1. Burn TOC file w/ cdrdao '--multi' option
         2. Finalize disc with dummy data session command:

            cdrecord --tsize=%ds /dev/zero

         #########################################################
         """ % (cd_obj.last_index.len_.frames-2))    # see note for '-2'

   @staticmethod
   def _open_file(name,mode='rb',encoding=None):
      """Wrapper for opening files. Ensures correct encoding is selected."""
      try:
         if encoding is None:
            # detect file character encoding
            with open(name,mode) as fh:
               d = chardet.universaldetector.UniversalDetector()
               for line in fh.readlines():
                  d.feed(line)
               d.close()
               encoding = d.result['encoding']
         return codecs.open(name, mode, encoding=encoding)
      except:
         print >> sys.stderr, sys.exc_value
         exit(-1)

   def _check_multisession_opt(self, cd, opt):
      """Check multi-session run-time options match track info."""
      if not cd.disc.is_multisession:
         return
      if opt.no_multisession:
         # disable multi-session
         cd.disc.is_multisession = False
      elif not opt.multisession:
         # multisesssion option must be set to prevent usage error
         print >> sys.stderr, textwrap.dedent("""
            WARNING! - Detected multi-session track info.

            For safety, '%s' option must be specified when creating a TOC
            for a multi-session disc.

            If you want to ignore this check, and disable multi-session
            features, use the '%s' argument.""" %
               (_OPT_MULTI_SESSION,_OPT_IGNORE_MULTI_SESSION))
         sys.exit(-1)

   def _banner_msg(self):
      """Returns a TOC comment header that is placed at the top of the
      TOC file."""
      return "// Generated by %s %s\n" % (progName, VERSION) + \
         "// %s, %s\n" % (__copyright__, __author__) + \
         "// Report bugs to <%s>\n" % __email__

   def _parse_args(self,argv):
      """Use OptionParser object to handle all input arguments and
      return opt structure and args list as a tuple. All argument
      error checking is performed in this function."""
      usage = '[OPTIONS] [[-f] CUE_FILE|-w WAV_FILES] [[-o] TOC_FILE]'
      parser = OptionParser( usage='%prog '+usage, version='%prog '+VERSION,
                             conflict_handler='resolve')
      parser.add_option('--help', action='callback',
            callback=self._parse_full_help,
            help='show detailed usage instructions and exit' )
      parser.add_option( _OPT_ALLOW_WAV_FNF, '--allow-missing-wav',
            dest='find_wav', action="store_false", default=True,
            help='do not abort when WAV file(s) are missing, (experts only)')
      parser.add_option( _OPT_OFFSET_CORRECT, '--offset-correction',
            dest='wav_offset', type='int',
            help='correct reader/writer offset by creating WAV file(s) '
                 'shifted by WAV_OFFSET samples (original data is '
                 'not modified)' )
      parser.add_option('-d', '--debug', dest='debug', action="store_true",
            default=False, help='enable debugging statements' )
      parser.add_option( _OPT_CUE_FILE, '--file', dest='cue_file',
            help='specify the input CUE file to read')
      parser.add_option( _OPT_MULTI_SESSION, '--multi', dest='multisession',
            action='store_true', default=False,
            help='for safety, this option must be set when creating a '
                 'mulit-session TOC file' )
      parser.add_option('-o', '--output', dest='toc_file',
            help='specify the output TOC file to write')
      parser.add_option( _OPT_TEMP_WAV, '--use-temp', dest='write_tmp',
            action='store_true', default=False,
            help='write offset corrected WAV files to /tmp directory' )
      parser.add_option( _OPT_WAV_LIST, '--wave', dest='wav_files',
            action='callback', callback=self._parse_wav,
            help='write a TOC file using list of WAV files' )
      parser.add_option( _OPT_IGNORE_MULTI_SESSION, '--no-multi',
            dest='no_multisession', action='store_true', default=False,
            help='disable multi-session support; program assumes TOC will be '
                 'written in single-session mode' )
      # execute parsing step
      opt,args = parser.parse_args(argv)

      # test "WAV file not found" and "offset correction" argument combination
      if opt.wav_offset and not opt.find_wav:
         parser.error("Can not combine '%s' and '%s' options!" % \
                        (_OPT_ALLOW_WAV_FNF,_OPT_OFFSET_CORRECT) )
      # test "offset correction" and "temp WAV" argument combination
      if opt.write_tmp and not opt.wav_offset:
         parser.error("Can not use '%s' without '%s' option!" % \
                        (_OPT_TEMP_WAV, _OPT_OFFSET_CORRECT) )
      # test "CUE File" and "-w" argument combination
      if opt.cue_file is not None and opt.wav_files is not None:
         parser.error("Can not combine '%s' and '%s' options!" % \
                        (_OPT_CUE_FILE, _OPT_WAV_LIST) )
      # test "--multi" and "--no-multi" argument combination
      if opt.multisession and opt.no_multisession:
         parser.error("Can not combine '%s' and '%s' options!" % \
                        (_OPT_MULTI_SESSION, _OPT_IGNORE_MULTI_SESSION) )
      # The '-w' option is used to create a TOC file using a list of WAV files.
      # The default mode is to convert a CUE file. The 'if' checks for the
      # default mode.
      if opt.wav_files is None:
         # the '-w' flag is not set. 1st argument names the input CUE file, 2nd
         # argument names the output TOC file.
         if len(args)>2 or \
               (len(args)>=1 and opt.cue_file) or \
               (len(args)>=2 and opt.toc_file):
            parser.error("Ambiguous file arguments!")
         else:
            # set file names if 'args' list is not empty
            if len(args)>=1: opt.cue_file = args[0]
            if len(args)>=2: opt.toc_file = args[1]
      else:
         # the '-w' flag was set. Only 1 argument is allowed to name the output
         # TOC file.
         if len(args)>1:
            parser.error("Ambiguous file arguments!")
         else:
            # set file names if 'args' list is not empty
            if len(args)>=1: opt.toc_file = args[0]
      return opt,args

   def _parse_full_help(self, option, opt_str, value, parser):
      """Callback to print a detailed help output page."""
      import mktoc
      parser.exit( msg=mktoc.__doc__)

   def _parse_wav(self, option, opt_str, value, parser):
      """OptionParser callback function to correctly handle '-w' WAV
      file input arguments. All trailing WAV files will be placed in
      the parser.values.wav_files list. The operation will stop at
      either the first non-WAV file argument or the end of the
      argument list."""
      slice_idx = 0
      for arg in parser.rargs:
         if re.search( r'\.wav$', arg, re.IGNORECASE):
            slice_idx += 1
         else: break

      # if one or more WAV files were found, move them to the parser wav_file
      # list
      if slice_idx:
         parser.values.wav_files = parser.rargs[0:slice_idx]
         del parser.rargs[0:slice_idx]
      else:
         parser.error( '%s option requires one or more WAV file arguments' \
                        % opt_str )

   def _error_msg(self, e):
      """Print a default error message to the user."""
      print >> sys.stderr, textwrap.dedent((u"""
      ERROR! -- An unrecoverable error has occurred.

      If you believe the CUE file is correct, please send the input file to
      <%s>, along with the error message below.

      ---> %s
      """ % (__email__,e)).encode('utf-8'))

   def _error_msg_multi_files(self, e):
      """Print error when duplicate WAV files are found."""
      print >> sys.stderr, textwrap.dedent( (u"""
      ERROR! -- Could not resolve WAV file:
         '%s'\n""" % (e.src_file,)).encode('utf-8'))

      print >> sys.stderr, "   Conflicting matches are:"
      for f in e.found_files:
         print >> sys.stderr, '      ' + unicode(f).encode('utf-8')

      print >> sys.stderr, textwrap.dedent( """
      Cdrdao can not correctly write pregaps in TOC files without explicit
      file lengths. If you know what you are doing, you can disable this
      check with the '%s' option.""" % (_OPT_ALLOW_WAV_FNF,))

   def _error_msg_no_file(self, e):
      """Print a missing WAV file error message to the user."""
      print >> sys.stderr, textwrap.dedent( (u"""
      ERROR! -- Could not find the WAV file:
         '%s'

      Cdrdao can not correctly write pregaps in TOC files without explicit
      file lengths. If you know what you are doing, you can disable this
      check with the '%s' option.""" % (e,_OPT_ALLOW_WAV_FNF,)).encode('utf-8'))


def main():
   """
   Primary entry point for the mktoc command line application.

   Creates a :class:`CommandLine` object and catches any exceptions. Returns 0
   to indicate success, or any other value for failure.
   """
   global progName
   progName = os.path.basename(sys.argv[0])
   try:
      CommandLine().run()
   except EmptyCueData: pass     # ignore NULL data input (Ctrl-C)
   except Exception:
      traceback.print_exc()
   except: pass      # ignore base exceptions (exit,key-int)
   else: return 0    # no exception, exit success
   return 1          # exit with failure
