#=============================================================================
#
#  Copyright (c) Kitware, Inc.
#  All rights reserved.
#  See LICENSE.txt for details.
#
#  This software is distributed WITHOUT ANY WARRANTY; without even
#  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
#  PURPOSE.  See the above copyright notice for more information.
#
#=============================================================================
"""Export script for ProteusPoisson applications"""

import os
import sys
import smtk
if 'pybind11' == smtk.wrappingProtocol():
  #print 'Using pybind11 bindings'
  import smtk.attribute
  import smtk.model
  import smtk.simulation
  import smtk.view

# so devs don't complain about pyc files in source
sys.dont_write_bytecode = True

# ---------------------------------------------------------------------
# Format info structures & initializers
class FormatInfo:
  def __init__(self, lvalue, attribute_type, item_path, format_string='%s', \
    comment=None, write_if_none=False, insert_blank_line=True, \
    standard_format=True):
    self.lvalue = lvalue
    self.attribute_type = attribute_type
    self.item_path = item_path
    self.standard_format = standard_format
    self.args = [lvalue, attribute_type, item_path] # shorthand
    self.kwargs = {
      'comment': comment,
      'format_string': format_string,
      'write_if_none': write_if_none,
      'insert_blank_line': insert_blank_line
    }
fmt = FormatInfo

# ---------------------------------------------------------------------
#
# Output format list for physics file
#
pfile_formats = [
  fmt('name', 'physicsname', 'name', format_string='\"%s\"'),
  fmt('T', 'physicsTime', 'T'),
  fmt('bcsTimeDependent', 'bcsTimeDependent', 'bcsTimeDependent'),
  fmt('genMesh', 'gridalternate', 'genMesh'),
  fmt('LevelModelType', 'LevelModelType', 'LevelModelType', \
    format_string='Transport.%s')
]

# ---------------------------------------------------------------------
#
# Output format list for numerics file
#
nfile_formats = [
  fmt('stepController', 'time', 'time/stepController', \
    format_string='StepControl.%s', comment='Step controller class'),

  fmt('timeIntegration', 'time', 'time/timeIntegration', \
    format_string='TimeIntegration.%s', comment='Time integration class'),

  fmt('DT', 'time', 'time/timestep', comment='The time step'),

  fmt('nDTout', 'time', 'time/numtimesteps',  \
    comment = '# Number Of Output Time Steps'),

  fmt('nltol_u', 'tolerancesalternate', 'nltol_u', \
    comment='Global tolerances'),
  fmt('ltol_u', 'tolerancesalternate', 'ltol_u', \
    insert_blank_line=False),
  fmt('nl_atol_res', 'tolerancesalternate', 'nl_atol_res', \
    insert_blank_line=False),
  fmt('l_atol_res', 'tolerancesalternate', 'l_atol_res', \
    insert_blank_line=False),

  fmt('femSpaces', 'femspaces', 'femSpaces', format_string='{0:%s}', \
    comment = '# Finite element space', write_if_none=True),

  fmt('fullNewtonFlag', 'solver', 'solver/fullNewtonFlag', \
    comment='Boolean to do full Newton or modified Newton'),

  fmt('quadrature', 'quadrature', None, standard_format=False),

  fmt('multilevelNonlinearSolver', 'solver', \
    'solver/multilevelNonlinearSolver', format_string='NonlinearSolvers.%s'),

  fmt('nonlinearSmoother', 'solver', 'solver/nonlinearSmoother', \
    format_string='NonlinearSolvers.%s'),

  fmt('massLumping', 'numerics_tools', 'massLumping', \
    comment='Boolean to dump mass matrix'),

  fmt('nStagesTime', 'time', 'time/numstages', \
    comment='The number of stages for the time discretization'),

  #fmt(['nnx', 'nny', 'nnz'], 'gridalternate', \
  #    'gridinput/geometry/{nnx,nny,nnz}', \
  #  comment ='# Number of nodes in xyz'),

  fmt('nLevels', 'multilevelmeshlevels', 'nLevels', \
    comment='# Number of levels in mesh'),

  fmt('nonlinearSolverNorm', 'solver', 'solver/nonlinearSolverNorm', \
    format_string='nonlinearSolverNorm(%s)', \
    comment='Norm to use for nonlinear algebraic residual'),

  fmt('reactionLumping', 'numerics_tools', 'reactionLumping', \
    comment='Boolean to lump reaction term'),

  fmt('runCFL', 'time', 'time/runCFL', \
    comment='The maximum CFL for the time step'),

  fmt('timeOrder', 'time', 'time/timeOrder', \
    comment='The order of time discretization'),

  fmt('triangleOptions', 'gridalternate', 'triangleOptions', \
    format_string='\"%s\"',
    comment='Options string for triangle or tetGen'),

  fmt('numericalFluxType', 'numerics_tools', 'numericalFluxType', \
    format_string='NumericalFlux.%s'),

  fmt('subgridError', 'numerics_tools', 'subgridError', \
    standard_format=False, comment='# Stability or shock capturing'),

  fmt('shockCapturing', 'numerics_tools', 'shockCapturing', \
    standard_format=False)
]


# ---------------------------------------------------------------------
def ExportCMB(spec):
    '''Entry function, called by CMB to write export files

    Returns boolean indicating success
    Parameters
    ----------
    spec: Top-level object passed in from CMB
    '''
    #print 'Enter ExportCMB()'

    # Create local class to store spec values and other info
    ExportScope = type('ExportScope', (object,), dict())
    scope = ExportScope()
    scope.manager = spec.getSimulationAttributes()
    scope.model = None
    if scope.manager is not None:
      scope.model = scope.manager.refModelManager()
    #scope.gridinfo = None
    scope.nd = 3  # model dimension
    # if scope.model is not None:
    #   scope.gridinfo = scope.model.gridInfo()
    #   api_status = smtk.model.GridInfo.ApiStatus()
    #   nd = scope.gridinfo.dimension(api_status)
    #   if smtk.model.GridInfo.OK != api_status.returnType:
    #     msg = 'ERROR calling GridInfo.dimension(): %s' % \
    #       api_status.errorMessage
    #     print msg
    #     logger.addError(msg)
    #   else:
    #     scope.nd = nd
    scope.export_manager = spec.getExportAttributes()
    scope.logger = spec.getLogger()

    # Generate lookup table for <attribute definition, UI panel>
    scope.view_table = create_view_table(scope)

    # Set filename defaults
    scope.pfilename = 'poisson_p.py'
    scope.nfilename = 'poisson_n.py'


    nfile_item = find_instance(scope, 'ExportSpec', 'NumericsFile', \
      scope.export_manager)

    # Write physics file
    if nfile_item is not None:
      noext = nfile_item.value(0).split('.')[0]
      # take the numerics filename, strip the '_n' if it's there, use '_p'
      if noext.endswith('_n') or noext.endswith('_N'):
        noext = noext[:-1] + 'p'
      else:
        noext = noext + '_p'
      scope.pfilename = noext + '.py'
    success = write_pfile(scope)
    print 'Completed pfile?', success
    print 'logger has %d records, hasErrors? %s'  % \
      (scope.logger.numberOfRecords(), scope.logger.hasErrors())

    # Write numerics file
    if nfile_item is not None:
      scope.nfilename = nfile_item.value(0)
    success &= write_nfile(scope)
    print 'Completed nfile?', success
    print 'logger has %d records, hasErrors? %s'  % \
      (scope.logger.numberOfRecords(), scope.logger.hasErrors())


    print
    print 'Wrote files:', scope.pfilename, scope.nfilename

    return success


# ---------------------------------------------------------------------
def write_pfile(scope):
  '''Generates physics file

  Parameters
  ----------
  scope: ExportScope
         Top-level objects & data
  '''
  pfile_complete = False
  with open(scope.pfilename, 'w') as scope.file:
    print 'Writing ', scope.pfilename

    pfile_header = [
      '# Proteus Poisson physics file',
      '# Generated by Kitware CMB'
    ]
    write_lines(scope, pfile_header, insert_blank_line=False)

    # Write python imports
    write_lines(scope, [
      'from proteus import *',
      'from proteus.default_p import *'
      ])

    # Model dimension
    write_lines(scope, [
      '# Spatial dimension',
      'nd = %d' % scope.nd
      ])

    # todo: disabled until write_polyfile and getNativeModelName are fixed
    #write_polyfile(scope)

    # Write items specified in pfile_formats
    for info in pfile_formats:
      print '  write', info.lvalue
      if info.standard_format:
        write_format_info_instance(scope, *info.args, **info.kwargs)
      else:
        msg = 'No code to handle custom type %s' % info.lvalue
        print 'WARNING:', msg
        logger.addWarning(msg)
        continue

    # initial conditions - check timeIntegration
    time_int_item = find_instance(scope, 'time', 'time/timeIntegration')
    if time_int_item is None:
      log_missing_item(scope, 'time', 'time/timeIntegration')
    else:
      time_int = time_int_item.valueAsString(0)
      #print 'timeIntegration value: ', time_int
      if time_int == 'NoIntegration':
        ti_lines = [
          '# Steady state, so no initial conditions',
          'initialConditions = None'
        ]
        write_lines(scope, ti_lines)
      else:
          write_lines(scope, ['# TODO Initial conditions'])

    # sparse diffusion representation
    sd_item = find_instance(scope, 'numerics_other', 'othernumerics/matrix')
    print '  write sd_item', sd_item.valueAsString(0)
    sd_value = None
    sd_lines = []
    if sd_item and sd_item.valueAsString(0) == 'SparseMatrix':
      sd_value = True
      sd_lines.append('# Using sparse matrix representation')
    else:
      sd_value = False
      sd_lines.append('# Using dense matrix representation')
    ui_comment = create_ui_comment(scope, 'numerics_other', sd_item)
    sd_lines.append(ui_comment)
    sd_lines.append('sd = %s' % sd_value)
    write_lines(scope, sd_lines)

    # identity tensor
    # Is this only needed when analyticSolution used?
    #write_lines(scope, [
    #  '# Identity tensor',
    #  'Ident = numpy.identity(nd, \'d\')'
    #  ])

    # Boundary conditions
    write_bcs(scope, 'dirichletConditions', 'dirichlet_bc', \
      comment='Boundary Conditions')
    write_bcs(scope, 'fluxBoundaryConditions', 'flux_bc')
    write_bcs(scope, 'weakDirichletConditions', 'weak_dirichlet_bc')

    # Also write empty dictionaries for unavailable BC's
    bc_lines = [
      'advectiveFluxBoundaryConditions = {}',
      'diffusiveFluxBoundaryConditions = {}',
      'stressFluxBoundaryConditions = {}'
    ]
    write_lines(scope, bc_lines)

    # Initial conditions
    write_ics(scope)

    # coefficient class
    coefficent_att = find_instance(scope, 'coefficients')
    if coefficent_att:
      # Write aOfX, fOfX, nc (nd written above)
      coef_lines = []
      coef_lines.append('# Number of components')
      coef_lines.append('nc = 1')

      # TODO: 'physics pde coefficient' and 'phyiscs rhs coefficient' cannot be found
      #     I don't think either exists the in the view right now
      # aOfX_item = find_item(scope, coefficent_att, 'physics pde coefficients')
      # aOfX = aOfX_item.valueAsString(0)
      # source = string_to_source_list(aOfX, 4)
      # coef_lines.extend(['', '# a(x)'])
      # ui_comment = create_ui_comment(scope, coefficent_att.type(), aOfX_item)
      # #print 'ui_comment', ui_comment
      # coef_lines.append(ui_comment)
      # coef_lines.append('def aOfX(x):')
      # coef_lines.extend(source)
      # coef_lines.append('aOfX_dict = {0: aOfX}')

      # fOfX_item = find_item(scope, coefficent_att, 'physics rhs coefficients')
      # #coef_lines.extend(['', '# f(x)', 'def fOfX():'])
      # #coef_lines.extend(source)
      # coef_lines.extend(['', '# f(x)'])
      # ui_comment = create_ui_comment(scope, coefficent_att.type(), aOfX_item)
      # #print 'ui_comment', ui_comment
      # coef_lines.append(ui_comment)
      # coef_lines.append('def fOfX(x):')
      # fOfX = fOfX_item.valueAsString(0)
      # source = string_to_source_list(fOfX, 4)
      # coef_lines.extend(source)
      # coef_lines.append('fOfX_dict = {0: fOfX}')

      physics_item = find_item(scope, coefficent_att, 'physics')
      ui_comment = create_ui_comment(scope, 'coefficients', physics_item)
      coef_lines.extend(['', '# equation coefficients', ui_comment])

      # Gather up args and kwargs
      args = ('aOfX_dict', 'fOfX_dict', 'nc', 'nd')
      # Keyword args specified by list of tuple
      # (argumnet name, item name, argument value)
      kwargs = [
        ('timeVaryingCoefficients', 'physics time varying coefficients', None)
      ]
      find_kwargs(scope, coefficent_att, kwargs)
      argument_string = build_argument_string(args, kwargs)

      # Same as above; cannot find physics_item
      # physics = physics_item.valueAsString(0)
      # coef = 'coefficients = TransportCoefficients.%s(%s)' % \
      #   (physics, argument_string)
      # coef_lines.append(coef)

      write_lines(scope, coef_lines)
    else:
      log_missing_item(scope, 'coefficient')


    pfile_complete = True
  return pfile_complete


# ---------------------------------------------------------------------
def write_nfile(scope):
  '''
  '''
  file_complete = False
  with open(scope.nfilename, 'w') as scope.file:
    print 'Writing ', scope.nfilename

    file_header = [
      '# Proteus Poisson numerics file',
      '# Generated by Kitware CMB'
    ]
    write_lines(scope, file_header, insert_blank_line=False)

    # Get filename of the _p file. We presume it is in the same dir
    pfilename = os.path.basename(scope.pfilename)

    # Write fixed imports
    python_imports =  [
      'from proteus import *',
      'from proteus.default_n import *',
      'from %s import *' % pfilename[:-3]  # strip ".py"
    ]
    write_lines(scope, python_imports)

    for info in nfile_formats:
      print '  write', info.lvalue
      if info.standard_format:
        write_format_info_instance(scope, *info.args, **info.kwargs)
      # Custom formatting logic:
      elif 'quadrature' == info.lvalue:
        write_quadrature(scope, info)
      elif 'shockCapturing' == info.lvalue:
        write_shockCapturing(scope, info)
      elif 'subgridError' == info.lvalue:
        write_subgridError(scope, info)
      else:
        msg = 'Unrecognized custom lvalue \"%s\"' % info.lvalue
        print 'ERROR: ', msg
        scope.logger.addError(msg)

    # Handle numerics_other as special case
    alt_name_dict = { 'tolFace': 'tolFac'}
    nu_item = find_instance(scope, 'numerics_other', 'othernumerics')
    if nu_item is not None:
      ui_comment = create_ui_comment(scope, 'numerics_other', nu_item)
      nu_list = [ui_comment]
      num_items = nu_item.numberOfItemsPerGroup()
      for i in range(num_items):
        subitem = smtk.attribute.to_concrete(nu_item.item(i))
        name = subitem.name()
        value = None
        if name in alt_name_dict:
          name = alt_name_dict.get(name)
        if subitem.type() == smtk.attribute.Item.GroupType:
          value = dict()
          num_components = subitem.numberOfItemsPerGroup()
          for j in range(num_components):
            comp_item = smtk.attribute.to_concrete(subitem.item(j))
            comp_value = get_item_value(comp_item)
            if comp_value is not None:
              value[comp_item.name()] = comp_value
        else:
          value = get_item_value(subitem)

        if value is not None:
          nu_list.append('%s = %s' % (name, value))
      write_lines(scope, nu_list)

  return True


# ---------------------------------------------------------------------
def write_polyfile(scope):
  '''Retrieves polyfile name (if any) from model and checks it exists
  '''
  # Get polyfile name
  polyfile_path = None
  polyfile_name = None
  polyfile_base = None
  polyfile_comment = None

  # Check if file specified in export attributes
  polyfile_item = find_instance(scope, 'ExportSpec', 'polyfile', \
    manager=scope.export_manager)
  if polyfile_item is not None:
    polyfile_name = polyfile_item.value(0)
    if len(polyfile_name.strip()) > 0:
      polyfile_comment = '# Using polyfile specified in export dialog'
      write_lines(scope, [polyfile_comment, 'polyfile = \"%s\"' % polyfile_name])
      return

  # Check if polyfile is the input/source for the cmb model
  if scope.model is not None:
    polyfile_path = scope.model.getNativeModelName()
    if not polyfile_path:
      msg = 'No polyfile (native model name) found'
      print 'WARNING:', msg
      scope.logger.addWarning(msg)
      polyfile_comment = '# ' + msg
      polyfile_path = None

  # Validate model_name as polyfile
  if polyfile_path is not None:
    polyfile_dir, polyfile_name = os.path.split(polyfile_path)
    polyfile_base, polyfile_ext = os.path.splitext(polyfile_name)

    # If .cmb file, substitute extension
    if polyfile_ext == '.cmb':
      polyfile_ext = '.poly'
      polyfile_name = polyfile_base + polyfile_ext
      polyfile_path = os.path.join(polyfile_dir, polyfile_name)

    msg = 'Looking for file %s' % polyfile_name
    print msg
    scope.logger.addDebug(msg)

    # Make list of directories to look for polyfile
    try_dirs = []
    # Full path
    if polyfile_dir is not None and polyfile_dir != '':
      try_dirs.append(polyfile_dir)
    # Current working directory
    cwd = os.getcwd()
    if cwd not in try_dirs:
      try_dirs.append(cwd)
    # Directory specified for physics file output
    pfile_dir = os.path.dirname(scope.pfilename)
    if pfile_dir is not None and pfile_dir != '' and pfile_dir not in try_dirs:
      try_dirs.append(pfile_dir)

    polyfile_found = False
    for try_dir in try_dirs:
      try_path = os.path.join(try_dir, polyfile_name)
      if os.path.isfile(try_path):
        polyfile_found = True
        #polyfile_comment = '# Found polyfile \"%s\" in directory %s' % \
        #  (polyfile_name, try_dir)
        # Sadly, leave path out of comment so that we can run tests
        polyfile_comment = '# Input polyfile'

    if not polyfile_found:
      msg = 'Unable to find polyfile \"%s\" in directories: %s' % \
        (polyfile_name, ', '.join(try_dirs))
      print 'WARNING:', msg
      polyfile_comment = '# Unable to find polyfile \"%s\" in directories:\n' % \
        polyfile_name
      for try_dir in try_dirs:
        polyfile_comment = polyfile_comment + '# %s\n' % try_dir
      polyfile_name = None

  polyfile_lines = [polyfile_comment]
  if polyfile_name is None:
    polyfile_lines.append('domain = None')
  else:
    polyfile_lines.extend([
      'domain = Domain.PiecewiseLinearComplexDomain()',
      'domain.readPoly(\"%s\")' % polyfile_base
      ])
  polyfile_lines.append('movingDomain = None')
  write_lines(scope, polyfile_lines)

# ---------------------------------------------------------------------
def write_bcs(scope, att_type, function_name, variable_name=None, comment=None):
  '''
  Writes one combined function for all boundary conditions of given type

  Parameters
  ----------
  att_type: string
  function_name: string
                  the name used in the function definition
  variable_name: string
                 the identifier that is assigned the function name
                 default is att_type
  comment: string optional
  '''
  bc_atts = scope.manager.findAttributes(att_type)
  #print 'bc_atts for type ', att_type, ': ', bc_atts
  if not bc_atts:
    return
  # Sort by attribute's id for consistent output
  bc_atts.sort(key=lambda att: att.id())

  fcn_list = list()
  if comment is not None:
    if not comment.startswith('#'):
      comment = '# %s' % comment.lstrip()
    fcn_list.append(comment)

  ui_comment = create_ui_comment(scope, att_type)
  fcn_list.append(ui_comment)

  fcn_list.append('def %s(x, flag):' % function_name)

  fcn_count = 0
  for bc_att in bc_atts:
    # Get list of model entity ids
    model_ent_list = bc_att.associations()
    if not model_ent_list:
      msg = 'No model entities associated with attribute %s - ignoring' % \
        bc_att.name()
      scope.logger.addWarning(msg)
      print 'WARNING: ', msg
      continue

    # Fetch function body
    item = bc_att.find('functionBody')
    if not item:
      msg = '%s item is not set - ignoring' % bc_att.name()
      scope.logger.addWarning(msg)
      print 'WARNING:', msg
      continue
    else:
      body_item = smtk.attribute.to_concrete(item)

      if not body_item.isSet(0):
        msg = '%s item is not set - ignoring' % bc_att.name()
        scope.logger.addWarning(msg)
        print 'WARNING:', msg
        continue

    body = body_item.valueAsString(0)
    if not body:
      msg = 'No function body found for attribute %s - ignoring' % \
        bc_att.name()
      scope.logger.addWarning(msg)
      print 'WARNING: ', msg
      continue

    # Generate list of model entity ids
    native_id_list = list()
    for model_ent in model_ent_list:
      native_id = get_native_id(scope, model_ent)
      native_id_list.append(int(native_id))

    # Add conditional statement for this BC (indent 1 level)
    condx = 'if' if fcn_count < 1 else 'elif'
    fcn_list.append('    %s flag in %s:' % (condx, native_id_list))

    # Add function body for this BC (indent 2 levels)
    body_list = string_to_source_list(body, initial_indent=8)
    fcn_list.extend(body_list)

    fcn_count += 1

  if variable_name is None:
    variable_name = att_type
  if fcn_count > 0:
    fcn_list.append('%s = {0: %s}' % (variable_name, function_name))
  else:
    fcn_list.append('%s = {}' % variable_name)

  write_lines(scope, fcn_list)


# ---------------------------------------------------------------------
def write_ics(scope):
  '''Writes initial conditions object
  '''
  att_type = 'initialCondition'
  ic_att = find_instance(scope, att_type)
  if ic_att is None:
    return

  comment = '# Initial conditions'
  ui_comment = create_ui_comment(scope, att_type)
  output_list = [comment, ui_comment]

  class_name = 'InitialConditionsCMB'
  # Class header
  output_list.append('class %s:' % class_name)

  # Class body
  # todo: skipped for now...no classBody?
  # item = ic_att.find('classBody')
  # body_item = smtk.attribute.to_concrete(item)
  # body_value = body_item.valueAsString(0)
  # body_list = string_to_source_list(body_value, 4)
  # output_list.extend(body_list)

  # Object
  object_init = 'initialConditione = {0: %s()}' % class_name
  output_list.append(object_init)

  write_lines(scope, output_list)


# ---------------------------------------------------------------------
def write_quadrature(scope, format_info):
  '''Writes elementQuadrature and elementBoundaryQuadrature output
  '''
  info_sets = [
    ('elementQuadrature',
      'QuadratureMethod', 'QuadratureOrder',
      '# Element quadrature'
    ),
    ('elementBoundaryQuadrature',
      'BoundaryQuadratureMethod', 'BoundaryQuadratureOrder',
      '# Boundary element quadrature'
    )
  ]

  # List of quadrature methods that take nd as input
  ND_METHOD_NAMES = (
    'CubeGaussQuadrature',
    'SimplexGaussQuadrature',
    'SimplexLobattoQuadrature'
  )

  for info_set in info_sets:
    lvalue, method_name, order_name, comment = info_set
    method_item = find_instance(scope, format_info.attribute_type, \
      method_name)
    if method_item is None:
      continue

    # Begin assembling the output lines
    output_list = list()
    ui_comment = create_ui_comment(scope, 'quadrature', method_item)
    output_list.extend([comment, ui_comment])

    # Generate constructor arguments
    method = method_item.valueAsString(0)
    kwarg_list = []
    # Some quadrature types take nd as input
    if method in ND_METHOD_NAMES:
      kwarg = ('nd', None, str(scope.nd))
      kwarg_list.append(kwarg)

    # All quadrature items may take order as input
    order_item = find_instance(scope, format_info.attribute_type, order_name)
    if order_item is not None and order_item.isEnabled():
      kwarg = ('order', None, order_item.valueAsString(0))
      kwarg_list.append(kwarg)

    arg_string = build_argument_string([], kwarg_list)
    line = '%s = %s(%s)' % (lvalue, method, arg_string)
    output_list.append(line)
    write_lines(scope, output_list)


# ---------------------------------------------------------------------
def write_shockCapturing(scope, info):
  '''Writes shockCapturing output
  '''
  # Define constructor arguments
  args = ['coefficients', 'nd']
  kwargs = [
    ('shockCapturingFlag', 'shockCapturingFlag', None),
    ('shockCapturingFactor', 'shockCapturingFactor', None),
    ('uSC', 'uSC', None),
    ('lag', 'lag', None),
    ('gradLag', 'gradLag', None),
    ('betaPower', 'betaPower', None)
  ]
  write_object(scope, info, 'ShockCapturing', args, kwargs)


# ---------------------------------------------------------------------
def write_subgridError(scope, info):
  '''Writes subgridError output
  '''
  args = ['coefficients', 'nd']
  kwargs = [
    ('stabFlag', 'stabFlag', None),
    ('lag', 'lag', None),
    ('delayLagSteps', 'delayLagSteps', None),
    ('hFactor', 'hFactor', None),
    ('g', 'g', None)
  ]
  write_object(scope, info, 'SubgridError', args, kwargs)


# ---------------------------------------------------------------------
def write_object(scope, info, module_name, args_list, kwargs_list):
  '''Writes code to initialize object of given type & arguments
  '''
  item = find_instance(scope, info.attribute_type, info.item_path)
  if item is None:
    return

  output_list = list()
  comment = info.kwargs.get('comment')
  if comment is not None:
    output_list.append(comment)
  ui_comment = create_ui_comment(scope, info.attribute_type, item)
  output_list.append(ui_comment)
  class_name = item.valueAsString(0)

  find_kwargs(scope, item, kwargs_list)
  argument_string = build_argument_string(args_list, kwargs_list)
  line = '%s = %s.%s(%s)' % \
    (info.lvalue, module_name, class_name, argument_string)
  output_list.append(line)
  write_lines(scope, output_list)


# ---------------------------------------------------------------------
def get_native_id(scope, model_entity):
  '''Retrieves the native id for given model entity

  The current logic uses the model names, checking for standard prefixes
  assigned by the vthCMBModelBuilder class. If none are found, a value
  of 0 is returned.
  '''
  # Interim version parses entity name, which is set in vtkCMBModelBuilder
  name = model_entity.name()
  prefix_list = ['DomainSet', 'boundary set ']
  for prefix in prefix_list:
    if name.startswith(prefix):
      return name[len(prefix):]

  # else
  msg = 'Unable to get native id for model entity \"%s\"' % name
  print 'WARNING:', msg
  scope.logger.addWarning(msg)
  return '0'


# ---------------------------------------------------------------------
def string_to_source_list(input, initial_indent=0):
  '''Reformats input string into list of indented python strings

  This is intended to be used with stings stored in StringItem instances.
  '''
  indent = ''
  if initial_indent > 0:
    indent_list = [' '] * initial_indent
    indent = ''.join(indent_list)

  working_input = input.strip()
  input_list = working_input.split('\n')
  output_list = list()
  for line in input_list:
    if len(line) > 0:
      output_list.append(indent + line)
    else:
      output_list.append('')  # don't indent blank lines
  return output_list


# ---------------------------------------------------------------------
def write_format_info_instance(scope, lvalue, att_type, item_path, \
  format_string='%s', comment=None, insert_blank_line=True, \
  write_if_none=False):
  '''
  '''
  debug = False
  #if lvalue == 'numericalFluxType':
  #  debug = True
  #  print 'debug on for att_type', att_type

  output_list = list()
  item = find_instance(scope, att_type, item_path)
  if item is None and not write_if_none:
    if debug: print 'item not found for', att_type, item_path
    return

  if not item.isEnabled() and not write_if_none:
    return

  if comment is not None:
    if not comment.startswith('#'):
      comment = '# %s' % comment
    output_list.append(comment)

  ui_comment = create_ui_comment(scope, att_type, item)
  if ui_comment is not None:
    output_list.append(ui_comment)

  if item is None:
    output_list.append('%s = None' % lvalue)
  else:
    if isinstance(lvalue, basestring):
      lvalue = [lvalue]

    num_values = 0
    for i in range(len(lvalue)):
      value = 'VALUE_NOT_FOUND'
      if debug:
        print 'debug value %d: %s' % (i, item.valueAsString(i))
      if item.type() == smtk.attribute.Item.VoidType:
        value = True
      elif not item.isSet(i):
        continue
      else:
        value = item.valueAsString(i)
      formatted_value = format_string % value
      formatted_line = '%s = %s' % (lvalue[i], formatted_value)
      output_list.append(formatted_line)
      num_values += 1

  if num_values > 0:
    write_lines(scope, output_list, insert_blank_line)


# ---------------------------------------------------------------------
def write_item_instance(scope, att_type, item_path, format, comment=None, \
  insert_blank_line=True, write_if_none=False):
  '''Writes item for single-instance attribute

    Parameters
    ----------
    scope: ExportScope
           Top-level objects & data
    att_type
    item_path
    format: string or list (list if item has multiple values)
    comment: optional
    insert_blank_line: optional
    write_if_none: optional
  '''
  output_list = list()
  item = find_instance(scope, att_type, item_path)
  if item is None and not write_if_none:
    return

  if comment is not None:
    output_list.append(comment)

  if item is None:
    output_list.append(write_if_none)
  else:
    if isinstance(format, basestring):
      format = [format]

    for i in range(len(format)):
      value = item.valueAsString(i)
      output_list.append(format[i] % value)

  write_lines(scope, output_list, insert_blank_line)


# ---------------------------------------------------------------------
def write_lines(scope, strings, insert_blank_line=True):
  '''Writes list of strings to output stream

  Parameters
  ----------
    strings: list(str)
             the input strings to be written
             do *not* include line endings in the input strings
    scope: ExportScope
           Top-level objects & data
    insert_blank_line: boolean optional
                       when true, insert a blank line before writing strings
  '''
  if insert_blank_line:
    strings.insert(0, '')
  strings.append('')
  scope.file.write('\n'.join(strings))

def write_comment(scope, comment):
  full_line = "\n# " + comment + '\n'
  scope.file.write(full_line)


# ---------------------------------------------------------------------
def find_instance(scope, att_type, item_path=None, manager=None):
  '''Finds singleton/instance attribute or item based on inputs
  If no item_path is specified, returns the attribute  or None if not found.
  If item_path specified, returns the item or None if not found

  Parameters
  ----------
    scope: ExportScope
           Top-level objects & data
    att_type: smtk.attribute.Attribute
              the attribute type to find (should be 1 and only 1)
    item_path: string optional
               string specifying item names to traverse below attribute
    manager: smtk.attribute.Manager optional
             alternate attribute manager to use (instead of scope.manager)
  '''
  if manager is None:
    manager = scope.manager
  att_list = manager.findAttributes(att_type)
  if not att_list:
    return None

  if len(att_list) > 1:
    msg = 'Attribute type \"', att_type, '\" is NOT a singleton.'
    scope.logger.addError(msg)
    print 'ERROR: ', msg
    return None

  att = att_list[0]
  if item_path is None:
    return att

  # else
  return find_item(scope, att, item_path)


# ---------------------------------------------------------------------
def find_item(scope, att, item_path):
  '''Finds Item based on specified path

  Parameters
  ----------
    scope: ExportScope
           Top-level objects & data
    att: smtk.attribute.Attribute
    item_path: string
               specifies item names, using forward-slash as delimiter
  '''
  item = None  # return value
  # Uses the fact that Attribute and GroupItem both have find() method
  parent = att
  item_list = item_path.split('/')

  for name in item_list:
    if hasattr(parent, 'find'):  # attribute & group items
      item = parent.find(name)
    elif hasattr(parent, 'activeChildItem'):  # value items
      # Search active children
      item = None
      for i in range(parent.numberOfActiveChildrenItems()):
        if parent.activeChildItem(i).name() == name:
          item = parent.activeChildItem(i)
          break
    if item is None:
      return None
    # else
    parent = smtk.attribute.to_concrete(item)

  # Return parent because it has already been converted to concrete object
  return parent


# ---------------------------------------------------------------------
def get_item_value(item, index=0):
  '''
  TODO handle RefItem type
  '''
  value = 'VALUE_NOT_FOUND'
  if item.type() == smtk.attribute.Item.VoidType:
    value = item.isEnabled()
  elif not item.isEnabled():
    value = None
  elif index > item.numberOfValues():
    value = None
  elif not item.isSet(index):
    value = None
  else:
    value = item.value(index)

  return value

# ---------------------------------------------------------------------
def find_kwargs(scope, parent, kwargs):
  '''
  Updates kwargs by retrieving items from parent instance

  Parameters
  ----------
  parent: smtk.attribute.Attribute or smtk.attribute.Item
          parent object to optional items
  kwargs: list of tuples
          format is (name, item name, value)
  '''
  for i,kwarg in enumerate(kwargs):
    kwname, item_name, kwvalue = kwarg
    item = find_item(scope, parent, item_name)
    if item and item.isEnabled():
      value = get_item_value(item)
      if value is not None:
        kwargs[i] = kwname, item_name, value


# ---------------------------------------------------------------------
def build_argument_string(args, kwargs, include_parens=False):
  '''Builds string representing arguments input to class or function

  Current code only works for scalar items (numberOfValues == 1)

  Parameters
  ----------
  args: list of values
  kwargs: list of tuples
          format is (name, item name, value)
  include_parens: boolean optional

  Does *not* include kwargs with value of None
  '''
  arg_list = []
  if include_parens:
    arg_list.append('(')

  # Positional arguments
  for arg in args:
    arg_list.append('%s' % arg)

  # Keyword arguments
  for kwtuple in kwargs:
    kwname, item_name, kwvalue = kwtuple
    if kwvalue is None:
      continue
    if isinstance(kwvalue, basestring):
      arg_list.append('%s=\"%s\"' % (kwname, kwvalue))
    else:
      arg_list.append('%s=%s' % (kwname, kwvalue))

  if include_parens:
    arg_list.append(')')

  return ', '.join(arg_list)


# ---------------------------------------------------------------------
def log_missing_item(scope, att_type, item_path=''):
  '''Constructs error message
  '''
  msg = 'Missing attribute or item, path: %s:%s' % (att_type, item_path)
  scope.logger.addError(scope.logger, msg)
  print 'ERROR - ', msg


# ---------------------------------------------------------------------
def create_view_table(scope):
  '''Constructs dictionary of <attribute type, views> for UI feedback

  Traverses TopLevel View and descendants to build lookup table
  '''
  #table = dict()
  #view = scope.manager.findTopLevelView()
  #update_view_info(table, view)

  """
  print
  print '\"attribute_type: [view_path]\"'
  def_types = table.keys()
  def_types.sort()
  for def_type in def_types:
    print '%s: %s' % (def_type, table[def_type])
    print
  print
  """
  #return table

  return scope.manager.views()

# ---------------------------------------------------------------------
def update_view_info(table, view, view_path=[]):
  '''Updates contents of table or view_path
  '''
  #print 'Enter update_view_info, view', str(view)
  if hasattr(view, 'numberOfSubViews'):
    # If a GroupView, process sub views recursively
    for i in range(view.numberOfSubViews()):
      subview = view.subView(i)
      next_view = smtk.attribute.to_concrete(subview)
      next_path = view_path[:]
      if subview.title() != '':
        next_path.append(subview.title())
      update_view_info(table, next_view, next_path)
  elif hasattr(view, 'numberOfInstances'):
    # If InstanceView, update table with attribute types
    for i in range(view.numberOfInstances()):
      instance = view.instance(i)
      att_type = instance.type()
      if att_type in table:
        print 'WARNING: Found same attribute type twice: ' + att_type
        continue
      table[instance.type()] = view_path
    #print table
  elif hasattr(view, 'numberOfDefinitions'):
    # If AttributeView, update table with definition types
    for i in range(view.numberOfDefinitions()):
      defn = view.definition(i)
      def_type = defn.type()
      if def_type in table:
        print 'WARNING: Found same definition type twice: ' + def_type
        continue
      table[def_type] = view_path
  else:
      print 'Skipping view with title ' + view.title() + ', type' + view.type()
  #print 'Exit update_view_info'

# ---------------------------------------------------------------------
def create_ui_comment(scope, att_type, item=None):
  '''
  '''
  path = None
  defn = scope.manager.findDefinition(att_type)
  if defn is None:
    return '# UI path not found for attribute type %s' % att_type

  path = scope.view_table.get(defn.type())
  while path is None:
    defn = defn.baseDefinition()
    if defn is None:
      break
    path = scope.view_table.get(defn.type())

  if path is None:
    return '# UI path not found for attribute type %s' % att_type

  comment = '# UI Tab %s:%s' % (path[0], path[1])

  if item is not None:
    label = item.definition().label()
    if not label:
      label = item.name()
    comment = comment + ', Label \"%s\"' % label

    if item.definition().advanceLevel(0) > 0:
      comment = comment + ' [Advanced]'

  return comment
