'''Implements the set of records provided by a dbd'''

import os, os.path
import ctypes

from . import mydbstatic   # Pick up interface to EPICS dbd files

from .recordbase import Record


__all__ = ['InitialiseDbd', 'LoadDbdFile', 'records']


# This class contains all the record types current supported by the loaded
# dbd, and is published to the world as epics.records.  As records are added
# (in response to calls to LoadDbdFile) they are automatically available to
# all targets.
class RecordTypes(object):
    def __init__(self):
        self.__RecordTypes = set()

    def GetRecords(self):
        return sorted(self.__RecordTypes)

    def _PublishRecordType(self, on_use, recordType, validate):
        # Publish this record type as a method
        self.__RecordTypes.add(recordType)
        setattr(self, recordType,
            Record.CreateSubclass(on_use, recordType, validate))

    # Checks whether the given recordType names a known valid record type.
    def __contains__(self, recordType):
        return recordType in self.__RecordTypes


# Every record type loaded from a DBD is present as an attribute of this
# class with the name of the record type.
#
# For example, to create an ai record, simply write
#
#   records.ai('NAME', DESC = 'A test ai record', EGU = 'V')
#
records = RecordTypes()


# This class uses a the static database to validate whether the associated
# record type allows a given value to be written to a given field.
class ValidateDbField:
    def __init__(self, dbEntry):
        self.dbEntry = mydbstatic.dbCopyEntry(dbEntry)
        self.__FieldInfo = None

    def __ProcessFieldInfo(self, name):
        desc = mydbstatic.dbGetPrompt(self.dbEntry)
        typ = mydbstatic.dbGetFieldType(self.dbEntry)
        group = mydbstatic.dbGetPromptGroup(self.dbEntry)

        n_choices = mydbstatic.dbGetNMenuChoices(self.dbEntry)
        if n_choices > 0:
            menu_void = mydbstatic.dbGetMenuChoices(self.dbEntry)
            menu_p = ctypes.cast(menu_void,
                ctypes.POINTER(ctypes.c_char_p * n_choices))
        else:
            menu_p = []
        return (desc, typ, group, menu_p)

    # Computes list of valid names and creates associated arginfo
    # definitions.  This is postponed quite late to try and ensure the menus
    # are fully populated, in other words we don't want to fire this until
    # all the dbd files have been loaded.
    def __ProcessDbd(self):
        # ordered dict of field_name -> arginfo
        self.__FieldInfo = {}
        status = mydbstatic.dbFirstField(self.dbEntry, 0)
        while status == 0:
            name = mydbstatic.dbGetFieldName(self.dbEntry)
            if name != 'NAME':
                self.__FieldInfo[name] = self.__ProcessFieldInfo(name)
            status = mydbstatic.dbNextField(self.dbEntry, 0)

    def FieldInfo(self):
        if self.__FieldInfo is None:
            self.__ProcessDbd()
        return self.__FieldInfo


    # This method raises an attribute error if the given field name is
    # invalid.  As an important side effect it also sets the database
    # cursor to the appropriate database descriptor.
    def ValidFieldName(self, name):
        if name not in self.FieldInfo():
            raise AttributeError('Invalid field name %s' % name)

    # This method raises an exeption if the given field name does not exist
    # or if the value cannot be validly written.
    def ValidFieldValue(self, name, value):
        # First check the field name is valid
        self.ValidFieldName(name)
        value = str(value)

        # Set the database cursor to the field
        status = mydbstatic.dbFirstField(self.dbEntry, 0)
        while status == 0:
            if mydbstatic.dbGetFieldName(self.dbEntry) == name:
                break
            status = mydbstatic.dbNextField(self.dbEntry, 0)

        # Now see if we can write the value to it
        message = mydbstatic.dbVerify(self.dbEntry, value)
        assert message == None, \
            'Can\'t write "%s" to field %s: %s' % (value, name, message)



# The same database pointer is used for all DBD files: this means that all
# the DBD entries are accumulated into a single large database.
_db = ctypes.c_void_p()

def LoadDbdFile(dbdfile, on_use = None):
    dirname, filename = os.path.split(dbdfile)

    # Read the specified dbd file into the current database.  This allows
    # us to see any new definitions.
    curdir = os.getcwd()
    if dirname:
        os.chdir(dirname)

    # We add <epics_base>/dbd to the path so that dbd includes can be resolved.
    status = mydbstatic.dbReadDatabase(
        ctypes.byref(_db), filename, '.:%s/dbd' % _epics_base, None)
    assert status == 0, 'Error reading database %s (status %d)' % \
        (dbdfile, status)

    os.chdir(curdir)


    # Enumerate all the record types and build a record generator class
    # for each one that we've not seen before.
    entry = mydbstatic.dbAllocEntry(_db)
    status = mydbstatic.dbFirstRecordType(entry)
    while status == 0:
        recordType = mydbstatic.dbGetRecordTypeName(entry)
        if not hasattr(records, recordType):
            validate = ValidateDbField(entry)
            records._PublishRecordType(on_use, recordType, validate)
        status = mydbstatic.dbNextRecordType(entry)
    mydbstatic.dbFreeEntry(entry)


def InitialiseDbd(epics_base, host_arch = None):
    global _epics_base
    _epics_base = epics_base

    mydbstatic.ImportFunctions(_epics_base, host_arch)
    LoadDbdFile('base.dbd')
