# -*- coding: utf-8 -*-

from functools import partial
from zipfile import ZipFile
import zipfile
import codecs
import tempfile
import itertools
import os
import gettext
import csv
try:
    from pkg_resources import resource_filename
except ImportError:
    resource_filename = None


from lxml import etree

from . import bufferedzip
from .utf8csv import UTF8CSVWriter


def convert_xml(source_file, dest_file, culture, gettext):
    """Convert from an AIRS 3.0 XML file to an iCarol CSV translation file

    :param source_file: A string with the file name of the AIRS 3.0 XML file to be converted.
    :param dest_file: destination csv file name. If this ends with .zip then the output will be put into a zip file.
    :param culture: the destination language for the translation file
    :param gettext: a translation function as generated by :code:`get_stdtranslation`


    If :code:`source_file` ends with :code:`.zip` then an attempt will be made to open the
    file as a zip file and the first file in the zip will be opened as the
    source xml file to be parsed.

    If :code:`dest_file` ends with :code:`.zip` then the csv file will be written to a zip
    file containing exactly one file with the same basename as :code:`dest_file` and
    the file extension of .csv.

    The output csv file will be encoded in UTF-8 and will be prefixed by a UTF-8 BOM.
    """

    # intialize a processed set
    processed = set()

    # get a `file` object to send to the xml parser
    xmlfile = _open_zipfile(source_file)

    # files can be huge and consume too much memory, parse the file iteratively
    iterable = etree.iterparse(xmlfile, events=('start', 'end'))

    # get start event for root element
    event, element = next(iterable)

    # get the generator for the 'Source' element
    converted = _convert_part(processed, processed, iterable, element, 'Source', culture, _=gettext)
    data = itertools.chain([_header_row], converted)

    # open destination file
    with open(dest_file, 'wb') as fd:
        if dest_file.endswith('.zip'):
            # write to zip file, this goes out to disk as a temporary file and then gets
            # inserted into the zip file after the fact.
            with bufferedzip.BufferedZipFile(fd, 'w', zipfile.ZIP_DEFLATED) as zip:
                _write_csv_to_zip(zip, data, os.path.basename(dest_file)[:-4] + '.csv')
        else:
            # write directly to CSV
            _write_csv(fd, data)


def _get_localedir():
    """Retrieve the location of locales.

    If we're built as an egg, we need to find the resource within the egg.
    Otherwise, we need to look for the locales on the filesystem or in the
    system message catalog.
    """
    locale_dir = ''
    # Check the egg first
    if resource_filename is not None:
        try:
            locale_dir = resource_filename(__name__, "/i18n")
        except NotImplementedError:
            # resource_filename doesn't work with non-egg zip files
            pass
    if not hasattr(os, 'access'):
        # This happens on Google App Engine
        return os.path.join(os.path.dirname(__file__), 'i18n')
    if os.access(locale_dir, os.R_OK | os.X_OK):
        # If the resource is present in the egg, use it
        return locale_dir

    # Otherwise, search the filesystem
    locale_dir = os.path.join(os.path.dirname(__file__), 'i18n')
    if not os.access(locale_dir, os.R_OK | os.X_OK):
        # Fallback on the system catalog
        locale_dir = os.path.normpath('/usr/share/locale')

    return locale_dir


def get_stdtranslation(domain="Airs2Icarol", languages=None,
                       localedir=_get_localedir()):
    """Get default translation function for given languages
    """

    t = gettext.translation(domain=domain,
                            languages=languages,
                            localedir=localedir, fallback=True)
    try:
        _ = t.ugettext
    except AttributeError:  # Python 3
        _ = t.gettext

    return _


def _open_zipfile(dest_file):
    """ Attempt to open the first file in a .zip file; fallback on just opening the dest_file
    """
    if dest_file.endswith('.zip'):
        try:
            zip = ZipFile(dest_file, 'r')
            files = zip.namelist()
            return zip.open(files[0], 'r')
        except zipfile.BadZipfile:
            pass

    return open(dest_file)


def _convert_part(error_log, processed, iterable, root, tagname, culture=None, agencynum=None, _=None):
    """Convert part of a AIRS XML 3.0 file to iCarol translation CSV format

    This is a generator function yielding rows of data for the CSV file
    """

    # this will be a CIOC NUM in this case
    key = None
    subagency_num = None

    # Files can be really big, we want to be able to release memory after
    # processing a section. Collect a list of elements to be removed
    to_remove = []

    # keep collecting parse events until EOF or we hit the end of the tag we started with
    while True:
        try:
            # collect XML parse events
            event, element = next(iterable)
        except StopIteration:
            # End of File, need to escape
            return

        if event == 'start' and element.tag in _airs_type_mapping:
            # Found the start of an AIRS XML Element that needs to be handled as a group
            # This will be one of Source, Agency, Site or SiteService

            # Files can be big
            to_remove.append(element)

            # perform recursive call for this record type, will generate iCarol CSV rows
            for row in _convert_part(error_log, processed, iterable, element, element.tag, culture, subagency_num or agencynum, _):
                yield row

        elif event == 'end' and element.tag == 'Key' and element.getparent() == root:
            # new Key element, we need to capture it for later use
            key = element.text

            # We are currently in an Agency, need to note that for subsequent recursion for new Record Types
            if tagname == 'Agency':
                subagency_num = key

        elif event == 'end' and element == root:
            # This is the end event for the current Record Type

            # get rid of already processed sub records that are taking up memory
            for el in to_remove:
                root.remove(el)

            oldtag = element.tag

            # if the "Record Type" is Source then there are no rows to generate
            if oldtag == 'Source':
                return

            # get the iCarol record type for this record
            record_type = _tag_to_type_map.get(tagname)

            # this record has a combined id of iCarol record type, agency key, current record key
            record_id = (record_type, agencynum, key)

            # AIRS XML requires duplicated SiteService records if a service is available at multiple sites,
            # skip over any more processing if we have already seen it.
            if record_id in processed:
                return

            # record that we have seen this id
            processed.add(record_id)

            # get the translation for the default join string
            joinstr = _(u'; ')

            # iterate over mapping definitions

            row = [agencynum or key, key, record_type, culture]
            type_mapping = _airs_type_mapping[tagname]
            for item in _field_order:
                try:
                    fn = type_mapping[item]
                except KeyError:
                    value = ''
                else:
                    value = fn(element, joinstr=joinstr, _=_)
                value = (value or '').strip()
                row.append(value)

            yield row

            # NOTE this return must happen on the end event when element is root
            return


def _write_csv(csvfile, data, **kwargs):
    # required to have all spreadsheet programs
    # understand Unicode
    csvfile.write(codecs.BOM_UTF8)

    kwargs['quoting'] = csv.QUOTE_ALL

    csvwriter = UTF8CSVWriter(csvfile, **kwargs)

    csvwriter.writerows(data)


def _write_csv_to_zip(zip, data, fname, **kwargs):
    csvfile = tempfile.TemporaryFile()

    _write_csv(csvfile, data, **kwargs)

    csvfile.seek(0)
    zip.writebuffer(csvfile, fname)
    csvfile.close()


def _physical_location_description(root_element, joinstr=u'; ', _=None):
    if not _:
        _ = lambda x: x

    cross_street_label = _(u'Cross Street: ')

    parts = []
    part = root_element.xpath('CrossStreet/text()')
    if part:
        parts.append(cross_street_label + part[0])

    part = root_element.xpath('PhysicalLocationDescription/text()')
    if part:
        parts.append(part[0])

    return joinstr.join(parts)


def _languages_offered(root_element, joinstr=u'; ', _=None):
    if not _:
        _ = lambda x: x

    dash_sep = _(u' - ')
    return joinstr.join(dash_sep.join(x.xpath('*/text()')) for x in root_element.xpath('Languages'))


def _eligibility(root_element, joinstr=u'; ', _=None):
    if not _:
        _ = lambda x: x

    age_label = _(u'Age Requirements: ')
    residency_label = _(u'Residency Requirements: ')

    parts = []

    part = root_element.xpath('OtherRequirements/text()')
    if part:
        parts.append(part[0])

    residency = root_element.xpath('ResidencyRequirements/text()')

    part = root_element.xpath('AgeRequirements/text()')
    if part:
        parts.append(age_label + part[0] + (joinstr if residency else u''))

    part = residency
    if part:
        parts.append(residency_label + part[0])

    return u'<br /><br />'.join(parts)


def _xpath_join(xpath, root_element, joinstr=u'; ', _=None):
    return joinstr.join(root_element.xpath(xpath)) or None


def _addr_line_1_join(xpath, root_element, joinstr=u'; ', _=None):
    return _xpath_join(xpath, root_element, '\n', _)


def _translated_type_xpath(translate_phrase, xpath, root_element, joinstr=u'; ', _=None):
    if isinstance(translate_phrase, basestring):
        translate_phrase = [translate_phrase]
    else:
        translate_phrase = translate_phrase
    if _ is not None:
        translate_phrase = [_(element) for element in translate_phrase]
    new_xpath = xpath % tuple(translate_phrase)
    return _xpath_join(new_xpath, root_element, joinstr, _)


def _sort_fn(value):
        return (value in _last_items, value)


# make a fake translation fn _ to pick up translation strings that will be translated at run time
_ = lambda x: x

_airs_type_mapping = {
    u'Source': {},
    u'Agency': {
        u'AgencyNamePublic': partial(_xpath_join, 'Name/text()'),
        u'AgencyNameAlternate': partial(_translated_type_xpath, [_('Legal Name'), _('Former Name')], 'AKA[not(./Description) or not(starts-with(./Description,"%s") or starts-with(./Description,"%s"))]/Name/text()'),
        u'AgencyDescription': partial(_xpath_join, 'AgencyDescription/text()'),

        u'PhoneTollFree': partial(_xpath_join, 'Phone[@TollFree = "true" and ./Type/text() = "Voice" and ./Description/text() = "Toll Free"]/PhoneNumber/text()'),
        u'PhoneNumberHotline': partial(_xpath_join, 'Phone[./Type/text() = "Voice" and ./Description/text() = "Crisis"]/PhoneNumber/text()'),
        u'PhoneNumberAfterHours': partial(_xpath_join, 'Phone[./Type/text() = "Voice" and ./Description/text() = "After Hours"]/PhoneNumber/text()'),
        u'PhoneNumberBusinessLine': partial(_xpath_join, 'Phone[./Type/text() = "Voice" and ./Description/text() = "Office"]/PhoneNumber/text()'),
        u'PhoneFax': partial(_xpath_join, 'Phone[./Type/text() = "Fax"]/PhoneNumber/text()'),
        u'PhoneTTY': partial(_xpath_join, 'Phone[./Type/text() = "TTY/TDD"]/PhoneNumber/text()'),

        u'WebsiteAddress': partial(_xpath_join, 'URL/Address/text()'),
        u'EmailAddressMain': partial(_xpath_join, 'Email/Address/text()'),

        u'IRSStatus': partial(_xpath_join, 'IRSStatus/text()'),
        u'LegalStatus': partial(_xpath_join, '@LegalStatus'),
        u'SourceOfFunds': partial(_xpath_join, 'SourceOfFunds/text()'),

        u'LastVerifiedOn': partial(_xpath_join, 'ResourceInfo/@DateLastVerified'),
        u'LastVerifiedByName': partial(_xpath_join, 'ResourceInfo/Contact/Name/text()'),
        u'LastVerifiedByTitle': partial(_xpath_join, 'ResourceInfo/Contact/Title/text()'),
        u'LastVerifiedByEmailAddress': partial(_xpath_join, 'ResourceInfo/Contact/Email/Address/text()'),
        u'LastVerifiedByPhoneNumber': partial(_xpath_join, 'ResourceInfo/Contact/Phone[Type/text() = "Voice"]/PhoneNumber/text()'),

        u'MainContactTitle': partial(_translated_type_xpath, _('Primary Executive'), 'Contact[@Type = "%s"]/Title/text()'),
        u'MainContactName': partial(_translated_type_xpath, _('Primary Executive'), 'Contact[@Type = "%s"]/Name/text()'),
        u'MainContactPhoneNumber': partial(_translated_type_xpath, _('Primary Executive'), 'Contact[@Type = "%s"]/Phone/PhoneNumber/text()'),
        u'MainContactEmailAddress': partial(_translated_type_xpath, _('Primary Executive'), 'Contact[@Type = "%s"]/Email/Address/text()'),

        u'PhysicalAddress1': partial(_addr_line_1_join, 'AgencyLocation/PhysicalAddress/*[self::PreAddressLine or self::Line1]/text()'),
        u'PhysicalAddress2': partial(_xpath_join, 'AgencyLocation/PhysicalAddress/Line2/text()'),
        u'PhysicalCity': partial(_xpath_join, 'AgencyLocation/PhysicalAddress/City/text()'),
        u'PhysicalStateProvince': partial(_xpath_join, 'AgencyLocation/PhysicalAddress/State/text()'),
        u'PhysicalCountry': partial(_xpath_join, 'AgencyLocation/PhysicalAddress/Country/text()'),
        u'PhysicalPostalCode': partial(_xpath_join, 'AgencyLocation/PhysicalAddress/ZipCode/text()'),

        u'MailingAddress1': partial(_addr_line_1_join, 'AgencyLocation/MailingAddress/*[self::PreAddressLine or self::Line1]/text()'),
        u'MailingAddress2': partial(_xpath_join, 'AgencyLocation/MailingAddress/Line2/text()'),
        u'MailingCity': partial(_xpath_join, 'AgencyLocation/MailingAddress/City/text()'),
        u'MailingPostalCode': partial(_xpath_join, 'AgencyLocation/MailingAddress/ZipCode/text()'),
        u'MailingCountry': partial(_xpath_join, 'AgencyLocation/MailingAddress/Country/text()'),
        u'MailingStateProvince': partial(_xpath_join, 'AgencyLocation/MailingAddress/State/text()'),

        u'InternalNotesForEditorsAndViewers': partial(_xpath_join, 'InternalNote/text()'),
        u'InternalNote': partial(_xpath_join, 'EditorsNote/text()'),
        u'Custom_PublicComments': partial(_xpath_join, 'PublicNote/text()'),
        u'Custom_FormerNames': partial(_translated_type_xpath, _('Former Name'), 'AKA/Description[starts-with(text(), "%s")]/../Name/text()'),
        u'Custom_LegalNames': partial(_translated_type_xpath, _('Legal Name'), 'AKA/Description[starts-with(text(), "%s")]/../Name/text()'),
    },
    u'Site': {
        u'AgencyNamePublic': partial(_xpath_join, 'Name/text()'),
        u'AgencyNameAlternate': partial(_translated_type_xpath, [_('Legal Name'), _('Former Name')], 'AKA[not(./Description) or not(starts-with(./Description,"%s") or starts-with(./Description,"%s"))]/Name/text()'),
        u'PhysicalAddress1': partial(_addr_line_1_join, 'PhysicalAddress/*[self::PreAddressLine or self::Line1]/text()'),
        u'PhysicalAddress2': partial(_xpath_join, 'PhysicalAddress/Line2/text()'),
        u'PhysicalCity': partial(_xpath_join, 'PhysicalAddress/City/text()'),
        u'PhysicalStateProvince': partial(_xpath_join, 'PhysicalAddress/State/text()'),
        u'PhysicalCountry': partial(_xpath_join, 'PhysicalAddress/Country/text()'),
        u'PhysicalPostalCode': partial(_xpath_join, 'PhysicalAddress/ZipCode/text()'),

        u'MailingAddress1': partial(_addr_line_1_join, 'MailingAddress/*[self::PreAddressLine or self::Line1]/text()'),
        u'MailingAddress2': partial(_xpath_join, 'MailingAddress/Line2/text()'),
        u'MailingCity': partial(_xpath_join, 'MailingAddress/City/text()'),
        u'MailingPostalCode': partial(_xpath_join, 'MailingAddress/ZipCode/text()'),
        u'MailingCountry': partial(_xpath_join, 'MailingAddress/Country/text()'),
        u'MailingStateProvince': partial(_xpath_join, 'MailingAddress/State/text()'),


        u'PhoneTollFree': partial(_xpath_join, 'Phone[@TollFree = "true" and ./Type/text() = "Voice" and ./Description/text() = "Toll Free"]/PhoneNumber/text()'),
        u'PhoneNumberHotline': partial(_xpath_join, 'Phone[./Type/text() = "Voice" and ./Description/text() = "Crisis"]/PhoneNumber/text()'),
        u'PhoneNumberAfterHours': partial(_xpath_join, 'Phone[./Type/text() = "Voice" and ./Description/text() = "After Hours"]/PhoneNumber/text()'),
        u'PhoneNumberBusinessLine': partial(_xpath_join, 'Phone[./Type/text() = "Voice" and ./Description/text() = "Office"]/PhoneNumber/text()'),
        u'PhoneFax': partial(_xpath_join, 'Phone[./Type/text() = "Fax"]/PhoneNumber/text()'),
        u'PhoneTTY': partial(_xpath_join, 'Phone[./Type/text() = "TTY/TDD"]/PhoneNumber/text()'),

        u'WebsiteAddress': partial(_xpath_join, 'URL/Address/text()'),
        u'EmailAddressMain': partial(_xpath_join, 'Email/Address/text()'),

        u'MainContactTitle': partial(_translated_type_xpath, _('Primary Contact'), 'Contact[@Type = "%s"]/Title/text()'),
        u'MainContactName': partial(_translated_type_xpath, _('Primary Contact'), 'Contact[@Type = "%s"]/Name/text()'),
        u'MainContactPhoneNumber': partial(_translated_type_xpath, _('Primary Contact'), 'Contact[@Type = "%s"]/Phone/PhoneNumber/text()'),
        u'MainContactEmailAddress': partial(_translated_type_xpath, _('Primary Contact'), 'Contact[@Type = "%s"]/Email/Address/text()'),

        u'DisabilitiesAccess': partial(_xpath_join, 'DisabilitiesAccess/text()'),
        u'HoursOfOperation': partial(_xpath_join, 'TimeOpen/Notes/text()'),

        u'AgencyDescription': partial(_xpath_join, 'SiteDescription/text()'),
        u'PhysicalLocationDescription': _physical_location_description,

        u'LanguagesOffered': _languages_offered,
        u'InternalNotesForEditorsAndViewers': partial(_xpath_join, 'InternalNote/text()'),
        u'InternalNote': partial(_xpath_join, 'EditorsNote/text()'),
        u'Custom_PublicComments': partial(_xpath_join, 'PublicNote/text()'),
        u'Custom_FormerNames': partial(_translated_type_xpath, _('Former Name'), 'AKA/Description[starts-with(text(), "%s")]/../Name/text()'),
        u'Custom_LegalNames': partial(_translated_type_xpath, _('Legal Name'), 'AKA/Description[starts-with(text(), "%s")]/../Name/text()'),
    },
    u'SiteService': {
        u'AgencyNamePublic': partial(_xpath_join, 'Name/text()'),
        u'AgencyNameAlternate': partial(_translated_type_xpath, [_('Legal Name'), _('Former Name')], 'AKA[not(./Description) or not(starts-with(./Description,"%s") or starts-with(./Description,"%s"))]/Name/text()'),
        u'AgencyDescription': partial(_xpath_join, 'Description/text()'),

        u'PhoneTollFree': partial(_xpath_join, 'Phone[@TollFree = "true" and ./Type/text() = "Voice" and ./Description/text() = "Toll Free"]/PhoneNumber/text()'),
        u'PhoneNumberHotline': partial(_xpath_join, 'Phone[./Type/text() = "Voice" and ./Description/text() = "Crisis"]/PhoneNumber/text()'),
        u'PhoneNumberAfterHours': partial(_xpath_join, 'Phone[./Type/text() = "Voice" and ./Description/text() = "After Hours"]/PhoneNumber/text()'),
        u'PhoneNumberBusinessLine': partial(_xpath_join, 'Phone[./Type/text() = "Voice" and ./Description/text() = "Office"]/PhoneNumber/text()'),
        u'PhoneFax': partial(_xpath_join, 'Phone[./Type/text() = "Fax"]/PhoneNumber/text()'),
        u'PhoneTTY': partial(_xpath_join, 'Phone[./Type/text() = "TTY/TDD"]/PhoneNumber/text()'),

        u'WebsiteAddress': partial(_xpath_join, 'URL/Address/text()'),
        u'EmailAddressMain': partial(_xpath_join, 'Email/Address/text()'),

        u'CoverageArea': partial(_xpath_join, 'GeographicAreaServed/Description/text()'),

        u'MainContactTitle': partial(_translated_type_xpath, _('Primary Contact'), 'Contact[@Type = "%s"]/Title/text()'),
        u'MainContactName': partial(_translated_type_xpath, _('Primary Contact'), 'Contact[@Type = "%s"]/Name/text()'),
        u'MainContactPhoneNumber': partial(_translated_type_xpath, _('Primary Contact'), 'Contact[@Type = "%s"]/Phone/PhoneNumber/text()'),
        u'MainContactEmailAddress': partial(_translated_type_xpath, _('Primary Contact'), 'Contact[@Type = "%s"]/Email/Address/text()'),

        u'LastVerifiedOn': partial(_xpath_join, 'ResourceInfo/@DateLastVerified'),
        u'LastVerifiedByName': partial(_xpath_join, 'ResourceInfo/Contact/Name/text()'),
        u'LastVerifiedByTitle': partial(_xpath_join, 'ResourceInfo/Contact/Title/text()'),
        u'LastVerifiedByEmailAddress': partial(_xpath_join, 'ResourceInfo/Contact/Email/Address/text()'),
        u'LastVerifiedByPhoneNumber': partial(_xpath_join, 'ResourceInfo/Contact/Phone[Type/text() = "Voice"]/PhoneNumber/text()'),

        u'FeeStructureSource': partial(_xpath_join, 'FeeStructure/text()'),
        u'ApplicationProcess': partial(_xpath_join, 'ApplicationProcess/Description/text()'),
        u'LanguagesOffered': _languages_offered,
        u'InternalNotesForEditorsAndViewers': partial(_xpath_join, 'InternalNote/text()'),
        u'InternalNote': partial(_xpath_join, 'EditorsNote/text()'),
        u'Custom_PublicComments': partial(_xpath_join, 'PublicNote/text()'),
        u'Eligibility': _eligibility,
        u'HoursOfOperation': partial(_xpath_join, 'TimeOpen/Notes/text()'),
        u'Custom_FormerNames': partial(_translated_type_xpath, _('Former Name'), 'AKA/Description[starts-with(text(), "%s")]/../Name/text()'),
        u'Custom_LegalNames': partial(_translated_type_xpath, _('Legal Name'), 'AKA/Description[starts-with(text(), "%s")]/../Name/text()'),

    }


}
_header_prefix = ['AgencyNUM', 'NUM', 'Record Type', 'CultureCode']

# Get all keys from dictionary
_field_order = [key for entry in _airs_type_mapping.values() for key in entry.keys()]

# Remove duplicates and place required elements at the end of the list
_last_items = ['Custom_PublicComments', 'Custom_FormerNames', 'Custom_LegalNames']
_field_order = list(sorted(set(_field_order), key=_sort_fn))

_header_row = _header_prefix + _field_order

del _

_tag_to_type_map = {
    'Agency': 'AGENCY', 'Site': 'SITE', 'SiteService': 'PROGRAM'
}

__all__ = ['convert_xml']
