#!/usr/bin/env python
# Copyright (C) 2013, Elphel.inc.
# process configuration of features
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
__author__ = "Andrey Filippov"
__copyright__ = "Copyright 2013, Elphel, Inc."
__license__ = "GPL"
__version__ = "3.0+"
__maintainer__ = "Andrey Filippov"
__email__ = "andrey@elphel.com"
__status__ = "Development"
class EzynqFeatures:
    #Modify for this class
    ERRORS={
            'ERR_NOT_A_VARIANT':   'Specified value is not a valid variant',
            'ERR_NOT_AN_INTEGER':  'Value is not an integer',
            'ERR_NOT_A_FLOAT':     'Value is not a float',
            'ERR_NOT_A_BOOLEAN':   'Value is not a boolean'
            }
    BOOLEANS=(('0','FALSE','DISABLE','DISABLED','N','OFF'),
              ('1','TRUE', 'ENABLE','ENABLED','Y','ON'))
#  defines - a list, order determines HTML output order
#  Each element has fields:
#  'NAME' - unique name to access this parameter
#  'CONF_NAME' - how it appears in configuration file, '@' may be replaced by str(channel)
#  'TYPE' - either "I" for integer, F - float, T - text, B- boolean (may be false/true, 0/1 or enable{d}/disable{d} or tuple with valid options for value
#  'MANDATORY' - boolean: this parameter is mandatory (either directly specified or derived from some others)
#  'DERIVED' - TBD later
#  'DEFAULT' - default value (to be suggested on error? Use if not mandatory parameter?
#  'DESCRIPTION' - Parameter description - use in error message?
    def _choice_val(self,t,value):
        if isinstance(t,tuple) :
            for c in t:
                if isinstance(c,int):
                    try:
                        if c==int(value,0):
                            return c
                    except:
                        pass
                elif isinstance(c,float):
                    try:
                        if c==float(value):
                            return c
                    except:
                        pass
                elif isinstance(c, bool):
                    if value in self.BOOLEANS[1]:
                        return True
                    elif value in self.BOOLEANS[0]:
                        return False
                elif isinstance(c, str):
                    try:
                        if c.upper()==value.upper():
                            return c
                    except:
                        pass
#                    return value
            else:
                return None
                         
        
 
    def __init__(self,defines,channel=0):
        self.defs={}
        for i,feature in enumerate(defines):
            self.defs[feature['NAME']]=feature
            self.defs[feature['NAME']]['INDEX']=i
        self.channel=channel
        self.pars={}
        self.target={} # target values (as specified)
        self.config_names={}
        self.defined=set()
        self.calculated=set()
        for name in self.defs:
            cn=self.defs[name];
            self.config_names[cn['CONF_NAME'].replace('@',str(channel))]=name
                            
            
        
    def parse_features(self,raw_configs):
        for line in raw_configs:
            conf_name =  line['KEY']
            value =      line['VALUE']
            value=str(value).upper()
            try:
                name=self.config_names[conf_name]
            except:
                continue
            feature= self.defs[name]
            if (value=='HELP'):
                try:
                    print conf_name+': '+feature['DESCRIPTION']
                except:
                    print conf_name+': description is not available'
                continue
            if isinstance(feature['TYPE'], tuple):
                val=self._choice_val(feature['TYPE'],value)
                if val is None:
                    raise Exception(self.ERRORS['ERR_NOT_A_VARIANT']+': '+line['VALUE'] +' is not a valid variant for parameter '+
                                    conf_name+'. Valid are:'+str(feature['TYPE']))
                else:
                    value=val
            elif (feature['TYPE']=='I') or (feature['TYPE']=='H'):
                try:
                    value= int(value,0)
                except:
                    if value == 'Y':
                        value=1
                    else:    
                        raise Exception(self.ERRORS['ERR_NOT_AN_INTEGER']+': '+line['VALUE'] +' is not a valid INTEGER value for parameter '+ conf_name)
            elif (feature['TYPE']=='F'):
                if value == 'Y':
                    value=1.0
                else:    
                    try:
                        value= float(value)
                    except:
                        raise Exception(self.ERRORS['ERR_NOT_A_FLOAT']+': '+line['VALUE'] +' is not a valid FLOAT value for parameter '+ conf_name)
            elif (feature['TYPE']=='B'):
                if value in self.BOOLEANS[1]:
                    value=True
                elif value in self.BOOLEANS[0]:
                    value=False
                else:
#                    print line['VALUE'],type(line['VALUE'])
#                    print line['VALUE'] in self.BOOLEANS[1]
                    raise Exception(self.ERRORS['ERR_NOT_A_BOOLEAN']+': '+line['VALUE'] +' is not a valid boolean value for parameter '+ conf_name+
                                    '. Valid for "True" are:'+str(self.BOOLEANS[1])+', for "False" - '+str(self.BOOLEANS[0]))
            elif (feature['TYPE']=='T'):
                if value == 'Y':
                    value='1'
                pass #keep string value
            self.pars[name]=value
            self.defined.add(name)
            self.target[name]=value
    #check after calculating derivative parameters                    
    def check_missing_features(self):
        all_set=True
        for name in self.defs:
            if (not name in self.pars):
                if self.defs[name]['MANDATORY']:
                    all_set=False
                    print "Configuration file is missing mandatory parameter "+self.defs[name]['CONF_NAME']+': '+self.defs[name]['DESCRIPTION']
                else:
                    if not self.defs[name]['DEFAULT'] is None:
                    # use default parameter
#                    print 'Adding default : ',name,'=', self.defs[name]['DEFAULT']
                        self.pars[name]=self.defs[name]['DEFAULT']
        return all_set
    def get_par_names(self):
#        name_offs=sorted([(name,self.registers[name]['OFFS']) for name in self.registers], key = lambda l: l[1])
#        print '---self.registers=',self.registers 
#        unsorted_name_offs=[(name,self.defs[name]['OFFS']) for name in self.registers]
#        print '---unsorted_name_offs=',unsorted_name_offs 
#        name_offs=sorted(unsorted_name_offs, key = lambda l: l[1]) 
#        print '---name_offs=',name_offs 
#        return [n for n in name_offs]
# sort register names in the order of addresses
#        name_index=sorted([(name,self.defs[name]['INDEX']) for name in self.pars], key = lambda l: l[1])
#        return [n for n in sorted([(name,self.defs[name]['OFFS']) for name in self.registers], key = lambda l: l[1])]
        return [n[0] for n in sorted([(name,self.defs[name]['INDEX']) for name in self.pars], key = lambda l: l[1])]
#TODO: Use SELECT for options?
    def get_par_confname(self,name):
        try:
            return self.defs[name]['CONF_NAME']
        except:
            raise Exception (name+' not found in self.defs') # should not happen with wrong data, program bug
    def get_par_description(self,name):
        try:
            return self.defs[name]['DESCRIPTION']
        except:
            raise Exception (name+' not found in self.defs') # should not happen with wrong data, program bug

    def get_par_value_or_none(self,name):
        try:
            return self.pars[name]
        except:
            try: 
                _=self.defs[name]['CONF_NAME']
            except:    
                raise Exception (name+' not found in self.defs') # should not happen with wrong data, program bug
            return 

    def get_par_value(self,name):
        try:
            return self.pars[name]
        except:
#            print 'name=',name
#            print self.pars
            try: 
                config_name=self.defs[name]['CONF_NAME']
            except:    
                raise Exception (name+' not found in self.defs') # should not happen with wrong data, program bug
            raise Exception (config_name+' is not defined, nor calculated')

    def get_par_value_or_default(self,name):
        try:
            return self.pars[name]
        except:
#            print 'name=',name
#            print self.pars
            try: 
                config_name=self.defs[name]['CONF_NAME']
            except:    
                raise Exception (name+' not found in self.defs') # should not happen with wrong data, program bug
            try:
                return self.defs[name]['DEFAULT']
            except:
                raise Exception (config_name+' is not defined, nor calculated and no default is provided')
        
    def get_par_target(self,name):
        try:
            return self.target[name]
        except:
            try: 
                config_name=self.defs[name]['CONF_NAME']
            except:    
                raise Exception (name+' not found in self.defs') # should not happen with wrong data, program bug
            raise Exception ('Target value for '+config_name+' is not defined')
    
    def is_known(self,name): # specified or calculated
        return (name in self.defined) or (name in self.calculated)

    def is_mandatory(self,name):
        try:
            return self.defs[name]['MANDATORY']
        except:
            raise Exception (name+' not found in self.defs') # should not happen with wrong data, program bug
    def is_specified(self,name): # directly specified
        return name in self.defined

    def undefine_parameter(self,name):
        if name in self.pars:
            self.pars[name]=None

    
    def set_calculated_value(self,name,value,force=True):
        if (not force) and (name in self.defined):
            config_name=self.defs[name]['CONF_NAME']
            raise Exception (name+' ('+config_name+') is specifically defined in configuration file, value will not change') # should not happen with wrong data, program bug
        else:
            self.pars[name]=value
            self.calculated.add(name)
            if name in self.defined:
                self.defined.remove(name)

    def set_max_value(self,name,value):
#        print 'set_max_value (',name,',',value,')'
        if (not self.is_known(name)) or (value>self.pars[name]):
            self.set_calculated_value(name,value,True)

    def set_min_value(self,name,value):
        if (not self.is_known(name)) or (value<self.pars[name]):
            self.set_calculated_value(name,value,True)
            
                               
    def html_list_features(self,html_file):
        if not html_file:
            return
        html_file.write('<table border="1">\n')
#        html_file.write('<tr><th>Configuration name</th><th>Value<br/>(Target)</th><th>Type/<br/>Choices</th><th>Mandatory</th><th>Origin</th><th>Default</th><th>Description</th></tr>\n')
        html_file.write('<tr><th>Configuration name</th><th>Value (Target)</th><th>Type/<br/>Choices</th><th>Mandatory</th><th>Origin</th><th>Default</th><th>Description</th></tr>\n')
#        print  self.get_par_names()
#        for name in self.pars:
        row_class="even"
        for name in self.get_par_names():
#            name=    self.config_names[conf_name]
            feature= self.defs[name]
            value=   self.get_par_value(name)
            if value is None:
                value='None'
            else:    
                if isinstance(value,int):
                    if (feature['TYPE']=='H'):
                        value=hex(value)
                    else:
                        value=str(value)
            try:
                target_value=self.get_par_target(name)
                if isinstance(target_value,int):
                    if (feature['TYPE']=='H'):
                        target_value=hex(target_value)
                    else:
                        target_value=str(target_value)
                if value != target_value:  # Do not show target_value if it is the same as actual
#                    value+='<br/>('+str(target_value)+')'
                    value='<b>'+str(value)+'</b> ('+str(target_value)+')'
            except: # target_value is not set
                pass
            if name in self.defined:
                origin="Defined"
            elif name in self.calculated:
                origin="Calculated"
            else:    
                origin="Default"
            if isinstance (feature['TYPE'],tuple):
                par_type='<select>\n'
                for t in feature['TYPE']:
                    par_type+='  <option>'+str(t)+'</option>\n'
#                    par_type+=('','<br/>')[i>0]+str(t)
                par_type+='</select>\n'    
            else:        
                par_type={'H':'Integer','I':'Integer','F':'Float','B':'Boolean','T':'Text'}[feature['TYPE']]

#            if name=='BAUD_RATE':
#                print value
            if row_class=="odd": row_class="even" 
            else:                row_class="odd"
            if (feature['TYPE']=='H') and isinstance(feature['DEFAULT'],int) and (feature['DEFAULT']>9):
                sDefault=hex(feature['DEFAULT'])
            else:            
                sDefault=str(feature['DEFAULT'])
            html_file.write('<tr class="'+row_class+'"><td><b>'+feature['CONF_NAME']+'</b></td><td>'+str(value)+'</td><td>'+par_type+
                            '</td><td>'+('-','Y')[feature['MANDATORY']]+'</td><td>'+origin+'</td><td>'+sDefault+'</td><td>'+feature['DESCRIPTION']+'</td></tr>\n')
        html_file.write('</table>\n')