#!/usr/bin/env python
# ----------------------------------------------------------------------
# Find IOCs providing an EPICS record using a web service
#
# $Id: findrecord.py,v 1.25 2016/07/01 12:15:30 lauk Exp $
# ----------------------------------------------------------------------

import getopt
import os
import socket
import sys
import urllib
import urllib2
import xml.sax

PROGRAM = os.path.basename(sys.argv[0])
CVS_REVISION = "$Revision: 1.25 $"
__version__ = CVS_REVISION

WS_URL = 'http://epics-boot-info.psi.ch/find-channel.aspx'
TIMEOUT = 15   # seconds


HEADER_FIELDS = {
    'active': (('RECORD', 40, 'Channel'),
               ('RECORD TYPE', 16, 'RecordType'),
               ('DESCRIPTION', 40, 'Description'),
               ('IOC', 25, 'IOC'),
               ('FACILITY', 8, 'Facility')),
    'deleted': (('RECORD', 40, 'Channel'),
                ('RECORD TYPE', 16, 'RecordType'),
                ('DESCRIPTION', 40, 'Description'),
                ('IOC', 25, 'IOC'),
                ('FACILITY', 8, 'Facility'),
                ('LAST SEEN', 19, 'LastSeenOn'),
                ('DELETED ON', 19, 'DeletedOn'))
}


class FindChannelHandler(xml.sax.ContentHandler):
    """
    SAX handler for parsing the XML of the find-channel.aspx web service.
    """

    attr_names = ('Channel', 'RecordType', 'Description', 'IOC', 'Facility', 'LastSeenOn', 'DeletedOn')

    def __init__(self, channel_callback):
        self._channel_callback = channel_callback

    def startElement(self, name, attrs):
        if name == 'entry':
            d = {}
            for a in FindChannelHandler.attr_names:
                d[a] = attrs.get(a, None)
            self._channel_callback(d)


class OutputProcessor:
    """
    A base class for processing results to output. This class defines the common interface.
    """

    def __init__(self, out):
        """
        Create a new instance.

        :param out: A stream to write to (any object with a ``write`` method, that takes a string).
        """
        self.__out = out

    def write(self, s):
        """
        Write ``s`` to this processors stream.

        :param s: The ``string`` to write.
        """
        self.__out.write(s.encode('utf-8'))

    def print_header(self):
        """
        API for printing a header.
        """
        pass

    def print_channel(self, channel):
        """
        API for handling a single result entry.

        :param channel: A ``dict`` with keys set from the XML handler.
        """
        pass

    def print_footer(self):
        """
        API for printing a footer
        """
        pass


class TableOutputProcessor(OutputProcessor):
    """
    An output processor that generates ASCII tables
    """

    def __init__(self, out, field_separator, header_labels, field_lengths, field_names):
        """
        Create a new instance.

        :param out: The stream to ``write`` to.
        :param field_separator: The field separator, separating columns from each other.
        :param header_labels: The labels for displaying a header line.
        :param field_lengths: The width of each column.
        """
        OutputProcessor.__init__(self, out)
        self._header_labels = dict(zip(field_names, header_labels))
        self._header_lines = dict(zip(field_names, tuple(['-' * x for x in field_lengths])))
        self._format = field_separator.join(['%%(%s)-%ds' % (f, n) for (f, n) in zip(field_names, field_lengths)]) + "\n"

    def print_header(self):
        self.write(self._format % self._header_labels)
        self.write(self._format % self._header_lines)

    def print_channel(self, channel):
        self.write(self._format % channel)


class ListPVsOutputProcessor(OutputProcessor):

    def __init__(self, out):
        OutputProcessor.__init__(self, out)

    def print_channel(self, channelDict):
        self.write(channelDict['Channel'])
        self.write('\n')


class Counter:
    """
    A simple counter, because for closures to work, Python needs an object, not a primitive.
    """
    def __init__(self, start_from=0):
        self.count = start_from

    def increment(self, amount=1):
        self.count += amount


def main():

    opts, args = getopt.getopt(sys.argv[1:], 'df:hi:lrt:u:vx',
                               ['deleted', 'exact-search', 'facility=', 'help', 'ioc=', 'limit=', 'listpvs', 'regex',
                                'timeout=', 'url=', 'version'])
    url = WS_URL
    timeout = TIMEOUT
    search_opts = {'source': 'active', 'limit': 100}
    display_opts = {'outputmode': 'table',
                    'header_fields': HEADER_FIELDS['active'],
                    'field_separator': '  '
    }
    for o, a in opts:
        if o in ('-d', '--deleted'):
            search_opts['source'] = 'deleted'
            timeout = 60
        if o in ('-f', '--facility'):
            search_opts['facility'] = a
        if o in ('-h', '--help'):
            usage(sys.stdout)
            return 0
        if o in ('-i', '--ioc'):
            search_opts['ioc'] = a
        if o in ('-l', '--listpvs'):
            display_opts['outputmode'] = 'listpvs'
        if o in ('--limit',):
            search_opts['limit'] = int(a)
            timeout = 60
        if o in ('-r', '--regex'):
            search_opts['match'] = 'regex'
        if o in ('-t', '--timeout'):
            timeout = float(a)
        if o in ('-u', '--url'):
            url = a
        if o in ('-v', '--version'):
            sys.stdout.write('%s - %s\n' % (PROGRAM, __version__))
            return 0
        if o in ('-x', '--exact-search'):
            search_opts['match'] = 'exact'
    if len(args) == 0:
        raise RuntimeError('No search pattern given. Try -h for help.')
    if len(args) > 1:
        raise RuntimeError('Too many arguments. Try -h for help.')
    display_opts['header_fields'] = HEADER_FIELDS[search_opts['source']]
    output_processor = create_output_processor(sys.stdout, display_opts)
    output_processor.print_header()
    pattern = args[0]
    num_results = Counter()  # need instance, because of closure

    def channel_callback(channel):
        num_results.increment()
        output_processor.print_channel(channel)
    
    search(url, pattern, search_opts, timeout, FindChannelHandler(channel_callback))
    output_processor.print_footer()
    if num_results.count and num_results.count == search_opts['limit']:
        sys.stderr.write('%s: WARNING! Number of search results exactly matches limit. Maybe there is more data...\n'
                         % PROGRAM)


def usage(out):
    out.write('''
Usage: findrecord [OPTIONS] PVSEARCH

OPTIONS:
  -d, --deleted                      Search for deleted records
  -f FACILITY, --facility FACILITY   Limit search to FACILITY
  -h, --help                         Display instructions and exit
  -i IOC, --ioc IOC                  Limit search to IOC
  -l, --listpvs                      Display only PV names (one per line)
  --limit COUNT                      Limit number of results to COUNT
                                     (default: 100, 0=no limit)
  -r, --regex                        Treat PATTERN as a regular expression
  -t SEC, --timeout SEC              Timeout in seconds (default = 15)
  -u URL, --url URL                  Set base URL of web service.
  -v, --version                      Display version and exit
  -x, --exact-search                 Disable wildcard searches


NOTE: PVSEARCH must be preceded with '--' if PVSEARCH itself starts
with a '-' (see examples below).

EXAMPLES:
  1.  Search for all PVs that contain 'VME' anywhere in the name
      (implicit wildcard search):
      findrecord VME

  2.  Search for all PVs that start with 'ARIDI-PCT':
      findrecord ARIDI-PCT%
      findrecord -r '^ARIDI-PCT'

  3.  Search for all PVs ending in ':CURRENT':
      findrecord %:CURRENT
      findrecord -r ':CURRENT$'

  4.  Search for all PVs containing '-PCT' (note the '--'):
      findrecord -- -PCT

  5.  Search for all PVs starting with 'ARIDI' and ending in ':CURRENT'
      (note that no implicit prefix and suffix % are added for you):
      findrecord ARIDI%:CURRENT
      findrecord -r '^ARIDI.*:CURRENT$'

  6.  Search for all PVs with 'ARIDI' anywhere in the name, followed by
      ':CURRENT' anywhere behind that (note the exmplicit prefix and
      suffix % around the search term):
      findrecord %ARIDI%:CURRENT%
      findrecord -r 'ARIDI.*:CURRENT'
''')


def create_output_processor(out, display_opts):
    if display_opts['outputmode'] == 'table':
        labels, widths, field_names = zip(*display_opts['header_fields'])
        result = TableOutputProcessor(out, display_opts['field_separator'], labels, widths, field_names)
    elif display_opts['outputmode'] == 'listpvs':
        result = ListPVsOutputProcessor(out)
    else:
        raise RuntimeError('Unsupported output mode: %s' % display_opts['outputmode'])
    return result


def search(url, pattern, search_opts, timeout, xml_handler):
    """
    Search the webservice at `url` for `pattern`.

    :param url: The URL to the web service.
    :param pattern: The pattern to search for.
    :param ioc: Limit search to this IOC
    :param timeout: The number of seconds to wait for an HTTP connection.
    :param xml_handler: The SAX content handler to use when parsing the HTTP response.
    """
    # Workaround for https://tracker.psi.ch/jira/browse/CTRLIT-3391
    if pattern[-1] == ':':
        if 'match' not in search_opts:
            pattern = '%' + pattern + '%'
        elif search_opts['match'] == 'regex':
            pattern = pattern + '.*'
        elif search_opts['match'] == 'exact':
            raise ValueError('Unfortunately you found a known bug. Your search pattern must not end in ":" when using the exact matching mode.')
    query = search_opts.copy()
    query['format'] = 'xml'

    full_url = '%s/%s?%s' % (url, urllib.quote(pattern), urllib.urlencode(query))
    # In Python 2.4 (SL5), `urlopen` does not have a `timeout` parameter.
    # We have to use the timeout mechanism of the `socket` module.
    socket.setdefaulttimeout(timeout)
    try:
        f = urllib2.urlopen(full_url)
        try:
            parser = xml.sax.make_parser()
            parser.setContentHandler(xml_handler)
            parser.parse(f)
        finally:
            f.close()
    except urllib2.HTTPError, e:
        if e.code == 500:  # internal server error
            raise RuntimeError('The web service encountered an error. Please inform Controls IT.')
        if e.code == 503:  # service not available
            raise RuntimeError('The web service is currently not available. Try again in a few minutes.')
        raise e  # nothing special; just raise it again


if __name__ == '__main__':
        print "a"
        sys.argv = ["findrecord.py", "AVG"]
        main()
