#!/usr/bin/python
#
# Copyright 2011 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Handy utility functions."""

__author__ = 'api.sgrinberg@gmail.com (Stan Grinberg)'

import codecs
import csv
import datetime
import htmlentitydefs
import re
import sys
import traceback
import urllib
from urlparse import urlparse
from urlparse import urlunparse

from adspygoogle.common.AuthToken import AuthToken
from adspygoogle.common.Buffer import Buffer
from adspygoogle.common.Errors import Error


_BASE_SOAP_TYPES = ['long', 'string', 'dateTime', 'float', 'int', 'boolean',
                    'base64Binary', 'double']


def ReadFile(f_path):
  """Load data from a given file.

  Args:
    f_path: str Absolute path to the file to read.

  Returns:
    str Data loaded from the file, None otherwise.
  """
  data = ''

  if f_path:
    fh = open(f_path, 'r')
    try:
      data = fh.read()
    finally:
      fh.close()
  return data


def GetDataFromCsvFile(loc):
  """Get data from CSV file, given its location.

  Args:
    loc: str Location of the CSV data file.

  Returns:
    list Data from CSV file.
  """
  rows = []
  try:
    data = urllib.urlopen(loc).read()
  except IOError, e:
    if str(e).find('[Errno url error] unknown url type: \'') > -1:
      data = ReadFile(loc)
    else:
      raise e
  for row in csv.reader(data.split('\n')):
    if row:
      rows.append(row)
  return rows[1:]


def GetDictFromCsvFile(loc):
  """Get data from CSV file as a dictionary, given its location.

  For each row, the first value will be used as a key mapped to the second value
  in the row.

  Args:
    loc: str Location of the CSV data file.

  Returns:
    dict Data from CSV file.
  """
  rows = {}
  try:
    data = urllib.urlopen(loc).read()
  except IOError, e:
    if str(e).find('[Errno url error] unknown url type: \'c\'') > -1:
      loc = 'file:///%s' % loc.replace('\\', '/').replace(':', '|')
      data = urllib.urlopen(loc).read()
    else:
      raise e
  for row in csv.reader(data.split('\n')):
    if row: rows[row[0]] = row[1]
  return rows


def PurgeLog(log):
  """Clear content of a given log file.

  If the file cannot be opened, Error is raised.

  Args:
    log: str Absolute path to a log file.
  """
  try:
    fh = open(log, 'w')
    try:
      fh.write('')
    finally:
      fh.close()
  except IOError, e:
    raise Error(e)


def GetErrorFromHtml(data):
  """Return error message from HTML page.

  Args:
    data: str HTML data.

  Returns:
    str Error message.
  """
  pattern = re.compile('\n')
  data = pattern.sub('', data)
  # Fetch error message.
  pattern = re.compile('<title>(.*)</title>|<TITLE>(.*)</TITLE>')
  msg = pattern.findall(data)
  if msg:
    for item in msg[0]:
      if item:
        msg = item
        # Cut unnecessary wording.
        pattern = re.compile('Error: |ERROR: ')
        msg = pattern.sub('', msg, count=1)
  else:
    pattern = re.compile('<body>(.*)</body>')
    msg = pattern.findall(data)
    if msg: msg = msg[0].rstrip('.')
  # Fetch detail for the error message
  pattern = re.compile(('<blockquote><H1>.*</H1>(.*)<p></blockquote>|'
                        '<H1>.*</H1>(.*)</BODY>'))
  msg_detail = pattern.findall(data)
  if isinstance(msg_detail, list) and msg_detail:
    for item in msg_detail[0]:
      if item: msg_detail = item
    msg_detail = msg_detail.strip('.')
    # Cut any HTML tags that appear in the message.
    pattern = re.compile('<.?H2>|<.?p>|<.?A.*>|<.?P.*>|<.?HR.*>')
    msg_detail = pattern.sub(' ', msg_detail).strip(' ')
    if msg_detail == msg: msg_detail = ''
  else:
    msg_detail = ''

  if msg:
    if not msg_detail: return '%s.' % msg
    return '%s. %s.' % (msg, msg_detail)
  else:
    # Check for non standard HTML content, with just the <body>.
    pattern = re.compile('<body>(.*)</body>')
    msg = pattern.findall(data)
    if msg: return msg[0]
  return ''


def IsHtml(data):
  """Return True if data is HTML, False otherwise.

  Args:
    data: str Data to check.

  Returns:
    bool True if data is HTML, False otherwise.
  """
  # Remove banners and XML header. Convert to lower case for easy search.
  data = ''.join(data.split('\n')).lower()
  pattern = re.compile('<html>.*?<body.*?>.*?</body>.*?</html>')
  if pattern.findall(data):
    return True
  else:
    return False


def DecodeNonASCIIText(text, encoding='utf-8'):
  """Decode a non-ASCII text into a unicode, using given encoding.

  A full list of supported encodings is available at
  http://docs.python.org/lib/standard-encodings.html. If unable to find given
  encoding, Error is raised.

  Args:
    text: str Text to encode.
    [optional]
    encoding: str Encoding format to use.

  Returns:
    tuple Decoded text with the text length, (text, len(text)).
  """
  if isinstance(text, unicode): return (text, len(text))

  dec_text = ''
  try:
    decoder = codecs.getdecoder(encoding)
    dec_text = decoder(text)
  except LookupError, e:
    msg = 'Unable to find \'%s\' encoding. %s.' % (encoding, e)
    raise Error(msg)
  except Exception, e:
    raise Error(e)
  return dec_text


def MakeTextXMLReady(text):
  """Convert given text into an XML ready text.

  XML ready text with all non-ASCII characters properly decoded.

  Args:
    text: str Text to convert.

  Returns:
    str Converted text.
  """
  dec_text = DecodeNonASCIIText(text)[0]
  items = []
  for char in dec_text:
    try:
      char = char.encode('ascii')
    except UnicodeEncodeError:
      # We have a non-ASCII character of type unicode. Convert it into an
      # XML-ready format.
      try:
        str(char)
        char.encode('utf-8')
      except UnicodeEncodeError:
        char = '%s;' % hex(ord(char)).replace('0x', '&#x')
    items.append(char)
  return ''.join(items)


def __ParseUrl(url):
  """Parse URL into components.

  Args:
    url: str URL to parse.

  Returns:
    tuple The components of the given URL.
  """
  return urlparse(url)


def GetSchemeFromUrl(url):
  """Return scheme portion of the URL.

  Args:
    url: str URL to parse.

  Returns:
    str The scheme portion of the URL.
  """
  return __ParseUrl(url)[0]


def GetNetLocFromUrl(url):
  """Return netloc portion of the URL.

  Args:
    url: str URL to parse.

  Returns:
    str Netloc portion of the URL.
  """
  return __ParseUrl(url)[1]


def GetPathFromUrl(url):
  """Return path portion of the URL.

  Args:
    url: str URL to parse.

  Returns:
    str Path portion of the URL.
  """
  return __ParseUrl(url)[2]


def GetServerFromUrl(url):
  """Return reconstructed scheme and netloc portion of the URL.

  Args:
    url: str URL to parse.

  Returns:
    str Scheme and netloc portion of given URL.
  """
  return urlunparse((GetSchemeFromUrl(url), GetNetLocFromUrl(url), '', '', '',
                     ''))


def GetAuthToken(email, password, service, lib_sig, proxy, login_token=None,
                 login_captcha=None):
  """Return an authentication token for Google Account.

  If an error occurs, AuthTokenError is raised.

  Args:
    email: str Google Account's login email.
    password: str Google Account's password.
    service: str Name of the Google service for which to authorize access.
    lib_sig: str Signature of the client library.
    proxy: str HTTP proxy to use.

  Returns:
    str Authentication token for Google Account.
  """
  return AuthToken(email, password, service, lib_sig, proxy, login_token,
                   login_captcha).GetAuthToken()


def GetCurrentFuncName():
  """Return current function/method name.

  Returns:
    str Current function/method name.
  """
  return sys._getframe(1).f_code.co_name


def UnLoadDictKeys(dct, keys_lst):
  """Return newly built dictionary with out the keys in keys_lst.

  Args:
    dct: dict Dictionary to unload keys from.
    keys_lst: list List of keys to unload from a dictionary.

  Returns:
    dict New dictionary with out given keys.
  """
  from adspygoogle.common import SanityCheck

  if not keys_lst: return dct
  SanityCheck.ValidateTypes(((dct, dict), (keys_lst, list)))

  new_dct = {}
  for key in dct:
    if key in keys_lst:
      continue
    new_dct[key] = dct[key]
  return new_dct


def CleanUpDict(dct):
  """Return newly built dictionary with out the keys that point to no values.

  Args:
    dct: dict Dictionary to clean up.

  Returns:
    dict New dictionary with out empty keys.
  """
  from adspygoogle.common import SanityCheck

  SanityCheck.ValidateTypes(((dct, dict),))

  new_dct = {}
  for key in dct:
    if dct[key]:
      new_dct[key] = dct[key]
  return new_dct


def BoolTypeConvert(bool_type, target=bool):
  """Convert bool to local bool and vice versa.

  Args:
    bool_type: bool or str a type to convert (i.e. True<=>'y', False<=>'n',
               'true'=>True, 'false'=>False).
    target: type (i.e. str or bool) for the return value, defaults to bool

  Returns:
    target boolean converted type.

  Raises:
    LookupError: Thrown when string value doesn't match one of: 1 y yes t true 0
                 n no f false
  """
  if isinstance(bool_type, target):
    return bool_type
  elif isinstance(bool_type, bool) and target == str:
    if bool_type:
      return 'y'
    else:
      return 'n'
  elif isinstance(bool_type, basestring):
    if bool_type.lower() in '1 y yes t true'.split():
      return target(True)
    elif bool_type.lower() in '0 n no f false'.split():
      return target(False)
    else:
      raise LookupError('unrecognized string boolean')
  else:
    return target(bool_type)


def LastStackTrace():
  """Fetch last stack traceback.

  Returns:
    str Last stack traceback.
  """
  # Temporarily redirect traceback from STDOUT into a buffer.
  trace_buf = Buffer()
  old_stdout = sys.stdout
  sys.stdout = trace_buf

  try:
    traceback.print_exc(file=sys.stdout)
  except AttributeError:
    # No exception for traceback exist.
    print ''

  # Restore STDOUT.
  sys.stdout = old_stdout
  return trace_buf.GetBufferAsStr().strip()


def HtmlUnescape(text):
  """Removes HTML or XML character references and entities from a text string.

  See http://effbot.org/zone/re-sub.htm#unescape-html.

  Args:
    text: str HTML (or XML) source text.

  Returns:
    str/unicode Plain text, as a Unicode string, if necessary.
  """

  def FixUp(m):
    """Helper function for HTML Unescaping."""
    text = m.group(0)
    if text[:2] == '&#':
        # character reference
      try:
        if text[:3] == '&#x':
          return unichr(int(text[3:-1], 16))
        else:
          return unichr(int(text[2:-1]))
      except ValueError:
        pass
    else:
      # named entity
      try:
        text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
      except KeyError:
        pass
    return text  # leave as is
  return re.sub('&#?\w+;', FixUp, text)


def HtmlEscape(text):
  """Escapes characters in a given text string.

  See http://wiki.python.org/moin/EscapingHtml.

  Args:
    text: str Text to HTML escape.

  Returns:
    str HTML escaped string.
  """
  html_escape_table = {
      '&': '&amp;',
      '"': '&quot;',
      '\'': '&apos;',
      '>': '&gt;',
      '<': '&lt;'
  }
  return ''.join(html_escape_table.get(char, char) for char in text)


def CsvEscape(text):
  """Escapes data entry for consistency with CSV format.

  The CSV format rules:
    - Fields with embedded commas must be enclosed within double-quote
      characters.
    - Fields with embedded double-quote characters must be enclosed within
      double-quote characters, and each of the embedded double-quote characters
      must be represented by a pair of double-quote characters.
    - Fields with embedded line breaks must be enclosed within double-quote
      characters.
    - Fields with leading or trailing spaces must be enclosed within
      double-quote characters.

  Args:
    text: str Data entry.

  Returns:
    str CSV encoded data entry.
  """
  if not text: return ''
  if text.find('"') > -1: text = text.replace('"', '""')
  if (not text or text.find(',') > -1 or text.find('"') > -1 or
      text.find('\n') > -1 or text.find('\r') > -1 or text[0] == ' ' or
      text[-1] == ' '):
    text = '"%s"' % text
  return text


def GetUniqueName(max_len=None):
  """Returns a unique value consisting of parts from datetime.datetime.now().

  Args:
    max_len: int Maximum length for the unique name.

  Returns:
    str Unique name.
  """
  dt = datetime.datetime.now()
  name = '%s%s%s%s%s%s%s' % (dt.microsecond, dt.second, dt.minute, dt.hour,
                             dt.day, dt.month, dt.year)
  if max_len > len(name): max_len = len(name)
  return name[:max_len]


def GetDictFromMap(entries):
  """Gets a dictionary from an array of map entries.

  A map entry is any object that has a key and value fields.

  Args:
    entries: list List of map entries.

  Returns:
    dict Dictionary constructed from map entries.
  """
  dct = {}
  for entry in entries:
    dct[entry['key']] = entry['value']
  return dct


def IsBaseSoapType(xsi_type):
  """Checks to see if a type is in the xsd or soapenc namespaces.

  This function is based on the fact that the WSDL-parsing scripts remove all
  namespaces except xsd and soapenc.

  Args:
    xsi_type: str The xsi type of an object from a WSDL.

  Returns:
    bool Whether or not the given type is in the xsd or soapenc namespaces.
  """
  return xsi_type in _BASE_SOAP_TYPES
