text: ###########################################################################
  #
  #  Copyright 2017 Google Inc.
  #
  #  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
  #
  #      https://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.
  #
  ###########################################################################
  """Handles interactions with the Buldozer feed.
  
  This module has all the column name mappings, reads and writes data to the
  Google Sheet that represents the Bulkdozer feed, and
  """
  
  import json
  import pytz
  
  from starthinker.util.sheets import sheets_get, sheets_read, sheets_write
  from dateutil import parser
  
  
  class FieldMap:
    """Contains static maps of column names and enumerations.
  
    This class maps user friendly field names with API enumeration values, it also
    maps Bulkdozer feed column names to constants so those can be referenced
    indirectly.
    """
    THIRD_PARTY_URL_TYPE_MAP = {
        'Start': 'VIDEO_START',
        'First quartile': 'VIDEO_FIRST_QUARTILE',
        'Midpoint': 'VIDEO_MIDPOINT',
        'Third quartile': 'VIDEO_THIRD_QUARTILE',
        'Complete': 'VIDEO_COMPLETE',
        'Mute': 'VIDEO_MUTE',
        'Pause': 'VIDEO_PAUSE',
        'Rewind': 'VIDEO_REWIND',
        'Full screen': 'VIDEO_FULLSCREEN',
        'Stop': 'VIDEO_STOP',
        'Custom click': 'CLICK_TRACKING',
        'Skip': 'VIDEO_SKIP',
        'Progress': 'VIDEO_PROGRESS'
    }
  
    AD_ID = 'Ad ID'
    AD_NAME = 'Ad Name'
    AD_START_DATE = 'Ad Start Date'
    AD_END_DATE = 'Ad End Date'
    AD_ACTIVE = 'Ad Active'
    AD_ARCHIVED = 'Ad Archived'
    AD_PRIORITY = 'Ad Priority'
    AD_LANDING_PAGE_ID = 'Landing Page ID'
    AD_CREATIVE_ROTATION_START_TIME = 'Start Date'
    AD_CREATIVE_ROTATION_END_TIME = 'End Date'
    AD_HARDCUTOFF = 'Hard Cutoff'
    AD_TYPE = 'Ad Type'
    CUSTOM_CLICK_THROUGH_URL = 'Custom URL'
  
    ADVERTISER_ID = 'Advertiser ID'
  
    ASSET_SIZE = 'Asset Size'
  
    CAMPAIGN_ID = 'Campaign ID'
    CAMPAIGN_NAME = 'Campaign Name'
    CAMPAIGN_START_DATE = 'Campaign Start Date'
    CAMPAIGN_END_DATE = 'Campaign End Date'
    CAMPAIGN_LANDING_PAGE_NAME = 'Landing Page Name'
    CAMPAIGN_LANDING_PAGE_URL = 'Landing Page URL'
    CAMPAIGN_LANDING_PAGE_ID = 'Landing Page ID'
  
    CAMPAIGN_CREATIVE_ASSOCIATION_ID = 'Campaign Creative Association ID'
    CREATIVE_ASSET_BUCKET_NAME = 'Creative Asset Bucket Name'
    CREATIVE_ASSET_ID = 'Creative Asset ID'
    CREATIVE_ASSET_NAME = 'Creative Asset Name'
    CREATIVE_ASSET_FILE_NAME = 'Creative Asset File Name'
  
    CREATIVE_NAME = 'Creative Name'
    CREATIVE_TYPE = 'Creative Type'
    CREATIVE_ID = 'Creative ID'
    CREATIVE_ROTATION = 'Creative Rotation'
    CREATIVE_ROTATION_SEQUENCE = 'Creative Rotation Sequence'
    CREATIVE_ROTATION_WEIGHT = 'Creative Rotation Weight'
    CREATIVE_WIDTH = 'Width (Display Only)'
    CREATIVE_HEIGHT = 'Height (Display Only)'
    
    CREATIVE_BACKUP_ASSET_ID = 'Backup Asset ID'
    CREATIVE_BACKUP_NAME = 'Creative Backup Name'
    BACKUP_IMAGE_FEATURES = 'Backup Image Features'
    BACKUP_IMAGE_TARGET_WINDOW_OPTION = 'Backup Image Target Window Option'
    BACKUP_IMAGE_CUSTOM_HTML = 'Backup Image Custom HTML'
    BACKUP_IMAGE_CLICK_THROUGH_LANDING_PAGE_ID = 'Backup Image Click Through Landing Page ID'
    BACKUP_IMAGE_CLICK_THROUGH_LANDING_PAGE_NAME = 'Backup Image Click Through Landing Page Name'
    CLICK_TAGS = 'clickTags'
    CLICK_TAG_NAME = 'Click Tag Name'
    CLICK_TAG_EVENT = 'Click Tag Event'
    CLICK_TAG_LANDING_PAGE_ID = 'Click Tag Landing Page ID'
    CLICK_TAG_LANDING_PAGE_NAME = 'Click Tag Landing Page Name'
  
  
    PLACEMENT_GROUP_ID = 'Placement Group ID'
    PLACEMENT_GROUP_NAME = 'Placement Group Name'
    PLACEMENT_GROUP_TYPE = 'Placement Group Type'
    PLACEMENT_GROUP_START_DATE = 'Placement Group Start Date'
    PLACEMENT_GROUP_END_DATE = 'Placement Group End Date'
    PLACEMENT_GROUP_PRICING_TYPE = 'Pricing Type'
  
    PLACEMENT_ID = 'Placement ID'
    PLACEMENT_NAME = 'Placement Name'
    PLACEMENT_START_DATE = 'Placement Start Date'
    PLACEMENT_END_DATE = 'Placement End Date'
    PLACEMENT_TYPE = 'Type'
    PLACEMENT_ARCHIVED = 'Archived'
    PLACEMENT_ACTIVE_VIEW_AND_VERIFICATION = 'Active View and Verification'
    PLACEMENT_AD_BLOCKING = 'Ad Blocking'
    PLACEMENT_PRICING_TESTING_START = 'Pricing Schedule Testing Starts'
    PLACEMENT_PRICING_COST_STRUCTURE = 'Pricing Schedule Cost Structure'
    PLACEMENT_PRICING_SCHEDULE_COST_STRUCTURE = 'Pricing Schedule Cost Structure'
    PLACEMENT_PRICING_SCHEDULE_RATE = 'Pricing Period Rate'
    PLACEMENT_PERIOD_START = 'Pricing Period Start Date'
    PLACEMENT_PERIOD_END = 'Pricing Period End Date'
    PLACEMENT_PERIOD_UNITS = 'Pricing Period Units'
    PLACEMENT_PERIOD_RATE = 'Pricing Period Rate'
    PLACEMENT_SKIPPABLE = 'Skippable'
    PLACEMENT_SKIP_OFFSET_SECONDS = 'Skip Offset Seconds'
    PLACEMENT_SKIP_OFFSET_PERCENTAGE = 'Skip Offset Percentage'
    PLACEMENT_PROGRESS_OFFSET_SECONDS = 'Progress Offset Seconds'
    PLACEMENT_ADDITIONAL_KEY_VALUES = 'Additional Key Values'
    PLACEMENT_PROGRESS_OFFSET_PERCENTAGE = 'Progress Offset Percentage'
  
    EVENT_TAG_NAME = 'Event Tag Name'
    EVENT_TAG_STATUS = 'Event Tag Status'
    EVENT_TAG_TYPE = 'Event Tag Type'
    EVENT_TAG_URL = 'Event Tag URL'
    EVENT_TAG_ID = 'Event Tag ID'
    EVENT_TAG_ENABLED_BY_DEFAULT = 'Enable By Default'
    EVENT_TAG_PROFILE_NAME = 'Event Tag Profile'
    EVENT_TAG_ENABLED = 'Enabled'
  
    SITE_ID = 'Site ID'
  
    TRANSCODE_ID = 'Transcode ID'
    TRANSCODE_MIN_WIDTH = 'Min Width'
    TRANSCODE_MAX_WIDTH = 'Max Width'
    TRANSCODE_MIN_HEIGHT = 'Min Height'
    TRANSCODE_MAX_HEIGHT = 'Max Height'
    TRANSCODE_MIN_BITRATE = 'Min Bitrate (kbps)'
    TRANSCODE_MAX_BITRATE = 'Max Bitrate (kbps)'
    TRANSCODE_FORMAT = 'Format'
  
    THIRD_PARTY_URL_TYPE = '3P URL Type'
    THIRD_PARTY_URL = '3P URL'
  
    LP_DAWN_CAMPAIGN_ID = 'CM Campaign ID'
    LP_DAWN_EMAIL = 'Email Address'
    LP_DAWN_LP_ID = 'CM Landing Page ID'
    LP_DAWN_URL = 'Desired Landing Page URL'
    LP_DAWN_DATE_FOR_UPDATE = 'Date for Update (Central Time)'
    LP_DAWN_TIME_FOR_UPDATE = 'Time for Update (Central Time)'
    LP_DAWN_STATUS = 'Status'
    LP_DAWN_UPDATE_TIME = 'Update Time'
    LP_DAWN_MESSAGE = 'Message'
  
    DYNAMIC_TARGETING_KEY_NAME = 'Key Name'
    DYNAMIC_TARGETING_KEY_OBJECT_TYPE = 'Object Type'
    DYNAMIC_TARGETING_KEY_OBJECT_ID = 'Object ID'
  
  class Feed:
    """Maps Bulkdozer feed items to and from dictionaries.
  
    """
    """Maps of internal feed names and Bulkdozer feed tabs.
  
    Each value is a list because if the first options is not found, it falls back
    to the subsequent one. E.g. lading_page_feed will first try to find a tab
    called "Landing Page", if that isn't found it will use the "Campaign" feed
    isntead, this allows end users to customize the tool behavior by adding or
    removing certain tabs.
    """
    _feed_name_tab_map = {
        'ad_feed': ['Ad'],
        'campaign_feed': ['Campaign'],
        'creative_asset_feed': ['Creative Asset'],
        'creative_feed': ['Creative'],
        'event_tag_feed': ['Event Tag'],
        'landing_page_feed': ['Landing Page', 'Campaign'],
        'placement_group_feed': ['Placement Group'],
        'placement_feed': ['Placement'],
        'third_party_url_feed': ['3P URLs'],
        'ad_creative_assignment_feed': ['Ad Creative Assignment', 'Ad'],
        'ad_placement_assignment_feed': ['Ad Placement Assignment', 'Ad'],
        'event_tag_ad_assignment_feed': ['Event Tag Ad Assignment', 'Ad'],
        'placement_pricing_schedule_feed': ['Placement Pricing Schedule'],
        'creative_asset_association_feed': [
            'Asset Creative Assignment', 'Creative'
        ],
        'creative_campaign_association_feed': [
            'Campaign Creative Assignment', 'Creative'
        ],
        'click_tag_feed': ['Click Tag'],
        'transcode_configs_feed': ['Transcode Configuration'],
        'event_tag_profile_feed': ['Event Tag Profile'],
        'lp_dawn': ['Form Responses 1'],
        'lp_dawn_status': ['Status'],
        'dynamic_targeting_key_feed': ['Dynamic Targeting Keys']
    }
  
    def __init__(self, auth, trix_id, feed_name, parse=True, spreadsheet=None, timezone=None):
      """Initializes the feed with parameters.
  
      Args:
        auth: The authentication scheme to use based on the json configuration
          file.
        trix_id: Unique identifier of the Google Sheet that represents the
          Bulkdozer feed.
        feed_name: The name of the feed to initialize.
        spreadsheet: Optional, the spreadsheet object representing the Bulkdozer
          feed spreadsheet, useful to limit calls to the sheets API and allow
          multiple Feed objects to use the same spreadsheet instance. This is used
          to determine which tabs exist in the feed so the correct one can be
          selected for the entity this Feed object represents.
      """
      self.auth = auth
      self.trix_id = trix_id
      self.trix_range = 'A1:AZ'
      self.feed_name = feed_name
      self._parse = parse
      self._timezone = timezone or 'America/New_York'
  
      # TODO: Make sure we only read the spreadsheet object or the list of tabs
      # once
      if spreadsheet:
        self.spreadsheet = spreadsheet
      else:
        self.spreadsheet = sheets_get(self.auth, self.trix_id)
  
      self.raw_feed = self._get_feed()
      self.feed = self._feed_to_dict(parse=self._parse)
  
    def update(self):
      """Updates the related Bulkdozer feed item with the values in this object.
  
      """
      new_feed = self._dict_to_feed(parse=self._parse)
  
      if len(new_feed) > 1:
        sheets_write(self.auth, self.trix_id, self.tab_name, self.trix_range, new_feed)
  
    def _get_feed(self):
      """Fetches the feed based on initialization parameters.
  
      Returns:
        List of lists that represents the rows and columns of the feed. If the
        feed isn't found returns a list with an empty list.
      """
  
      if self.feed_name in self._feed_name_tab_map:
        for tab_name in self._feed_name_tab_map[self.feed_name]:
          for sheet in self.spreadsheet['sheets']:
            if sheet['properties']['title'] == tab_name:
              self.tab_name = tab_name
              return sheets_read(self.auth, self.trix_id, tab_name, self.trix_range)
  
      return [[]]
  
    def _convert_date(self, value):
      """Converts dates into a Bulkdozer specific format to be written back to the Feed.
  
      Args:
        value: String representation of the date.
  
      Returns:
        Bulkdozer string representation of a date. Returns null if the value
        cannot be parsed into a date.
      """
      try:
        result = parser.parse(value)
  
        if not result.tzinfo:
          result = pytz.timezone(self._timezone).localize(result)
  
        if ':' in value:
          return result.strftime('%Y-%m-%dT%H:%M:%S.000%z')
        else:
          return result.strftime('%Y-%m-%d')
      except:
        return None
  
    def _convert_int(self, value):
      """Converts a value into a integer.
  
      Args:
        value: String representation of a field from the Bulkdozer feed.
  
      Returns:
        If possible to convert value into an integer, returns the integer
        representation, otherwise None.
      """
      try:
        return int(value)
      except:
        return None
  
    def _convert_float(self, value):
      """Conversta a value into a float.
  
      """
      try:
        return float(value)
      except:
        return None
  
    def _parse_value(self, value):
      """Parses a string value into a type specific value infering the correct type based on the data.
  
      Args:
        value: The value to parse.
  
      Returns:
        The representation of the value in the correct data type.
      """
      if isinstance(value, str):
        if value.upper() == 'TRUE':
          return True
        elif value.upper() == 'FALSE':
          return False
        else:
          int_value = self._convert_int(value)
  
          if int_value:
            return int_value
          else:
            float_value = self._convert_float(value)
  
            if float_value:
              return float_value
            else:
              date_value = self._convert_date(value)
  
              if date_value:
                return date_value
              else:
                return value
      else:
        return value
  
    def _dict_to_feed(self, parse=True):
      """Turns a feed into a list of strings to be written back to the feed.
  
      Args:
        feed: Dictionary list to convert into a list of lists of strings.
  
      Returns:
       List of list of strings representing the values of the feed.
  
      """
      if not self.raw_feed:
        return []
  
      headers = self.raw_feed[0]
  
      row = 1
  
      for item in self.feed:
        for key in iter(item.keys()):
          if key in headers:
            column = headers.index(key)
            value = self._parse_value(item[key]) if parse else item[key]
            while column >= len(self.raw_feed[row]):
              self.raw_feed[row].append('')
  
            self.raw_feed[row][column] = value
  
        row += 1
  
      return self.raw_feed
  
    def _feed_to_dict(self, parse=True):
      """Turns a raw feed from Google Sheets into a list of dictionaries.
  
      Args:
        raw_feed: List of list of strings representing the feed from Google
          Sheets.
  
      Returns:
        List of dictionaries with the data from the feed
      """
      if not self.raw_feed:
        return None
  
      result = []
  
      header = self.raw_feed[0]
  
      for line in self.raw_feed[1:]:
        if line and ''.join(line):
          i = 0
          item = {}
          for index, column_header in enumerate(header):
            if index < len(line):
              item[column_header] = self._parse_value(
                  line[index].strip()) if parse else line[index].strip()
            else:
              item[column_header] = ''
  
          result.append(item)
        else:
          break
  
      return result
  