#=============================================================================
#
#  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 ACE3P workflows
"""

# Internal variable for debugging
DEBUG_JOB_SUBMIT = False

import datetime
import logging
import os
import sys
from importlib import reload

sys.dont_write_bytecode = True

# Workaround for missing site-packages path in developer builds
site_path = None
py_folder = 'python{}.{}'.format(sys.version_info.major, sys.version_info.minor)
if sys.platform == 'win32':
  site_path = os.path.join(sys.prefix, 'bin', 'Lib', 'site-packages')
elif sys.platform == 'darwin':
  site_path = os.path.join(sys.prefix, 'lib', py_folder, 'site-packages')
elif sys.platform == 'linux':
  site_path = os.path.join(sys.prefix, 'lib', py_folder, 'site-packages')
else:
  print(f'Unrecognized platform {sys.platform}')
if site_path is not None:
  abs_path = os.path.abspath(site_path)
  if os.path.exists(abs_path) and abs_path not in sys.path:
      print(f'Adding to sys.path: {abs_path}')
      sys.path.append(abs_path)

# Make sure we can import all smtk modules used by the operation
try:
    import smtk
    import smtk.attribute
    import smtk.io
    import smtk.model
    import smtk.operation
except ImportError as err:
    print('ERROR importing smtk modules')
    print(str(err))
    print('sys.path:')
    for p in sys.path:
        print('  ', p)
    print()
    raise



# Add the directory containing this file to the python module search list
import inspect
source_file = os.path.abspath(inspect.getfile(inspect.currentframe()))
sys.path.insert(0, os.path.dirname(source_file))
# Make sure __file__ is set when using modelbuilder
__file__ = source_file


from internal.writers import utils, loading
from internal.writers import tem3pwriter  # for testing only

class Export(smtk.operation.Operation):

    def __init__(self):
        smtk.operation.Operation.__init__(self)
        self.output_file = None

    def name(self):
        return "export ACE3P"

    # Custom validation not required
    def ableToOperate(self):
        return smtk.operation.Operation.ableToOperate(self)

    def operateInternal(self):
        try:
            # Add/reset member data for result
            self.output_file = None  # set by ExportCMB()
            self.cumulus_job_id = None       # ditto
            self.cumulus_output_folder = None
            self.nersc_job_folder = None
            self.nersc_input_folder = None
            self.slurm_script = None
            success = ExportCMB(self)
        except Exception as err:
            self.log().addError('Exception => {}'.format(err))
            print('EXCEPTION:', str(err))
            print('LOG:', self.log().convertToString())
            result = self.createResult(smtk.operation.Operation.Outcome.FAILED)
            return result

        # Return with success
        result = self.createResult(smtk.operation.Operation.Outcome.SUCCEEDED)

        # Copy relevant info
        result_fields = [
            (self.output_file, 'OutputFile'),
            (self.cumulus_job_id, 'CumulusJobId'),
            (self.cumulus_output_folder, 'CumulusOutputFolder'),
            (self.nersc_job_folder, 'NerscJobFolder'),
            (self.nersc_input_folder, 'NerscInputFolder'),
            (self.slurm_script, 'SlurmScript')
        ]
        for t in result_fields:
            value, name = t
            if value is not None:
                item = result.find(name)
                if item is None:
                    # self.log().addWarning('Op result missing {} item'.format(name))
                    print('Op result missing {} item'.format(name))
                    pass
                else:
                    item.setIsEnabled(True)
                    item.setValue(value)
        return result

    def createSpecification(self):
        spec = self.createBaseSpecification()
        # print('spec:', spec)

        # Load export atts
        source_dir = os.path.abspath(os.path.dirname(__file__))
        print('source_dir:', source_dir)
        sbt_path = os.path.join(source_dir, 'internal', 'ace3p-export.sbt')
        print('sbt_path:', sbt_path)
        reader = smtk.io.AttributeReader()
        reader_err = reader.read(spec, sbt_path, self.log())
        # print('reader err?:', reader_err)
        if reader_err:
            print(self.log().convertToString())
            assert False, 'Error reading ace3p-export.sbt'

        # Special logic to copy GroupItem named "NERSCSimulation"
        # from "ace3p-submit" attribute defintion to "ace3p-export".
        # This so that we can reuse submit settings in both python
        # and C++ export operations.
        export_defn = spec.findDefinition('ace3p-export')
        assert export_defn is not None, 'Unable to find ace3p-export attdef'
        # pos = export_defn.findItemPosition('NERSCSimulation')
        # for i in range(export_defn.numberOfItemDefinitions()):
        #     item_def = export_defn.itemDefinition(i)
        #     print('  ', item_def.name())
        # assert pos >= 0, 'Unable to find NERSCSimulation placeholder'
        # nerscsim_defn = export_defn.itemDefinition(pos)

        # Load ace3p-submit.xml
        submit_path = os.path.join(source_dir, 'internal', 'ace3p-submit.xml')
        submit_text = None
        with open(submit_path) as f:
            submit_text = f.read()
        assert submit_text is not None, 'Unable to find/load {}'.format(submit_path)

        # Convert to full template
        start = '<SMTK_AttributeResource Version="4"><Definitions>'
        finish = '</Definitions></SMTK_AttributeResource>'
        sbt_text = start + submit_text + finish
        reader_err = reader.readContents(spec, sbt_text, self.log())
        if reader_err:
            print(self.log().convertToString())
            assert False, 'Error reading ace3p-export.sbt'

        # defns = spec.definitions()
        # print('DEFINITIONS:', defns)

        submit_defn = spec.findDefinition('ace3p-submit')
        assert submit_defn is not None, 'Unable to find ace3p-submit attdef'
        insert_defn = submit_defn.itemDefinition(0)
        assert insert_defn.name() == "NERSCSimulation", 'Unexpected item defn name {}'.format(insert_defn.name())
        assert insert_defn is not None, 'Unable to find NERSCSimulation group def'

        # nerscsim_defn.assign(insert_defn)
        added = export_defn.addItemDefinition(insert_defn)
        assert added, 'Failed to add (move) NERSCSimulation defn'

        # spec.removeDefinition(submit_defn)

        return spec

ExportScope = type('ExportScope', (object,), dict())
# ---------------------------------------------------------------------
def ExportCMB(export_op):
    '''Entry function, called by operator to write export files

    Returns boolean indicating success
    Parameters
    ----------
    spec: Attribute Resource specifying the export operation
    '''
    print('Enter ExportCMB()', export_op)

    reload(utils)

    # Initialize scope instance to store spec values and other info
    scope = ExportScope()

    scope.warning_messages = list()
    scope.logger = export_op.log()
    scope.export_att = export_op.parameters()

    # If NERSCSimulation enabled, apply workaround here
    nersc_item = scope.export_att.find('NERSCSimulation')
    if nersc_item is not None and nersc_item.isEnabled():
        # Load ssl library here. This is workaround for a problem
        # on macOS system when submitting jobs to NERSC. In some
        # cases, the requests module fails to use the CMB python
        # ssl and instead searches for a system ssl module.
        # The cause of the problem is unknown, but importing ssl
        # before certain other modules appears to suppress the problem.
        # So far, the symptom has been traced to importing email as a
        # dependency of pkg_resources.
        try:
            import ssl
        except ImportError as err:
            raise

    # Check for TestMode item
    scope.test_mode = False
    test_item = scope.export_att.findVoid('TestMode')
    if test_item is not None:
        scope.test_mode = test_item.isEnabled()

    scope.sim_atts = smtk.attribute.Resource.CastTo(scope.export_att.find('attributes').value())
    if scope.sim_atts is None:
        msg = 'ERROR - No simlation attributes'
        print(msg)
        raise Exception(msg)

    # Get categories
    analysis_att = scope.sim_atts.findAttribute('analysis')
    scope.categories = scope.sim_atts.analyses().getAnalysisAttributeCategories(analysis_att)
    print('categories', scope.categories)

    # Derive solver from the categories set
    solver = None
    cat = next(iter(scope.categories))  # returns first category (without removing it from the set)
    if cat.startswith('TEM3P'):
        solver = 'TEM3P'
    elif cat.startswith('Track3P'):
        solver = 'Track3P'
    elif len(scope.categories) == 1:
        solver = cat
    else:
        raise RuntimeError('Unexpected category set: {}'.format(scope.categories))

    # Get model resource
    scope.model_path = None
    if solver != 'Rf-Postprocess':
        ref_item = scope.export_att.find('model')
        resource = ref_item.value(0)
        if resource is None:
            msg = 'ERROR - No model'
            print(msg)
            raise RuntimeError(msg)
        scope.model_resource = smtk.model.Resource.CastTo(resource)

        # Get input model (to be uploaded if submitting)
        model_item = scope.export_att.findFile('MeshFile')
        if model_item is None:
            msg = 'MeshFile item not found -- cannot export'
            raise RuntimeError(msg)
        scope.model_path = model_item.value(0)
        print('scope.model_path %s' % scope.model_path)
        scope.model_file = os.path.basename(scope.model_path)

    # For now, keep using the solver_list
    scope.solver_list = [solver]
    print('solver:', solver)

    # Get output folder
    folder_item = scope.export_att.findDirectory('OutputFolder')
    if folder_item is None or not folder_item.isSet(0):
        msg = 'Output folder not set -- cannot export'
        scope.logger.addError(msg)
        raise Exception(msg)
    scope.output_folder = folder_item.value(0)
    # Create output folder if needed
    if not os.path.exists(scope.output_folder):
        os.makedirs(scope.output_folder)

    # Get output file prefix
    file_prefix_item = scope.export_att.findString('OutputFilePrefix')
    if file_prefix_item is None or not file_prefix_item.isSet(0):
        msg = 'Output file prefix not set -- cannot export'
        scope.logger.addError(msg)
        raise Exception(msg)
    scope.output_file_prefix = file_prefix_item.value(0)

    # Keep track of files & folders that need to get uploaded
    # Use a set to prevent inserting that same thing twice
    scope.files_to_upload = set()
    if scope.model_path is not None:
        scope.files_to_upload.add(scope.model_path)  # always
    scope.folders_to_upload = set()  # not currently used
    scope.symlink = None
    scope.nersc_directory = None
    scope.nersc_job_folder = None
    scope.nersc_input_folder = None

    # Loop over all solvers and write corresponding files
    for solver in scope.solver_list:
        if solver == 'acdtool':
            completed = True
            break

        file_ext = solver.lower()
        if file_ext == 'rf-postprocess':
            file_ext = 'rfpost'
        filename = '%s.%s' % (scope.output_file_prefix, file_ext)
        output_path = os.path.join(scope.output_folder, filename)
        export_op.output_file = output_path
        print('Output file %s' % output_path)

        completed = False
        with open(output_path, 'w') as scope.output:
            line = '// Generated by CMB'
            if not scope.test_mode:
                dt_string = datetime.datetime.now().strftime('%d-%b-%Y  %H:%M')
                line = '{} {}'.format(line, dt_string)
            if solver != 'Rf-Postprocess':
                scope.output.write(line)
                scope.output.write('\n')

            if solver == 'TEM3P':
                from internal.writers import tem3pwriter
                reload(tem3pwriter)
                writer = tem3pwriter.Tem3PWriter()
                writer.write(scope)
            elif solver == 'Track3P':
                # Track3P has a separate writer class
                from internal.writers import track3pwriter
                reload(track3pwriter)
                writer = track3pwriter.Track3PWriter()
                writer.write(scope)
            elif solver == 'Rf-Postprocess':
                from internal.writers import rfpostwriter
                reload(rfpostwriter)
                writer = rfpostwriter.RFPostWriter()
                writer.write(scope)
            else:
                write_modelinfo(scope)
                write_finiteelement(scope)
                write_pregion(scope)
                if solver == 'Omega3P':
                    write_eigensolver(scope)
                elif solver == 'S3P':
                    write_frequency_scan(scope)
                elif solver == 'T3P':
                    write_moving_window(scope)
                    write_checkpoint(scope)
                    write_loading_info(scope)
                    reload(loading)
                    loading.write_loading(scope)
                    write_time_stepping(scope)
                    write_monitor(scope)
                    write_linear_solver(scope)
                write_port(scope)
                write_postprocess(scope)
            print('Wrote output file %s' % output_path)
            completed = True
            scope.files_to_upload.add(output_path)

    print('Export completion status: %s' % completed)

    # In some runtime environments, stdout is null
    if sys.stdout is not None:
        sys.stdout.flush()
    if not completed:
        return completed

    # (else)
    # Check for NERSCSimulation item
    sim_item = scope.export_att.find('NERSCSimulation')
    if sim_item is not None and sim_item.isEnabled():
    # if sim_item is not None:
        if DEBUG_JOB_SUBMIT:
            import requests
            rdelay = requests.get('https://httpbin.org/delay/3')
            print(rdelay.text)
            r = requests.get('https://httpbin.org/uuid')
            print('uuid:', r.json().get('uuid'))
            return True

        # Check "UseNewtInterface" item
        newt_item = sim_item.find('UseNewtInterface')
        if newt_item is not None and newt_item.isEnabled():
            # Generate slurm script (only)
            from internal.writers import jobutils
            reload(jobutils)
            command_list = jobutils.create_slurm_commands(scope, sim_item)
            command_string = '\n'.join(command_list)

            # Note that the slurm script has ".in" extension because it
            # has a placeholder for $JOB_DIRECTORY, which cannot be set
            # until the user's scratch directory is known.
            script_name = '{}.sl.in'.format(scope.solver_list[0]).lower()
            script_path = os.path.join(scope.output_folder, script_name)
            with open(script_path, 'w') as f:
                f.write(command_string)
                f.write('\n')
                export_op.slurm_script = script_path
                print('Wrote slurm command file', script_path)
                completed = True

            export_op.nersc_job_folder = scope.nersc_job_folder
            export_op.nersc_input_folder = scope.nersc_input_folder
            return completed


        # Import nersc module (only when needed)
        from internal.writers import nersc
        reload(nersc)

        scope.cumulus = None
        completed = nersc.submit_ace3p(scope, sim_item)
        if scope.cumulus is not None:
            export_op.cumulus_job_id = scope.cumulus.job_id()
            export_op.cumulus_output_folder = scope.cumulus._output_folder_id
        export_op.nersc_job_folder = scope.nersc_job_folder
        export_op.nersc_input_folder = scope.nersc_input_folder

        print('Submit to NERSC status: %s' % completed)
    else:
        # For test/debug, list any files to upload
        print('Files for upload:')
        for filename in scope.files_to_upload:
            print('  ', filename)
        # And any sym link
        print('Symlink', scope.symlink)

    print('ACE3P.py number of warnings:', len(scope.warning_messages))
    for msg in scope.warning_messages:
        scope.logger.addWarning(msg)

    return completed

# ---------------------------------------------------------------------
def write_modelinfo(scope):
    '''Writes ModelInfo section to output stream

    Model info should have already been added to scope object
    '''
    scope.output.write('ModelInfo:\n')
    scope.output.write('{\n')

    # Always write model with ncdf extension
    # (If input is .gen file, must convert using acdtool)
    root, ext = os.path.splitext(scope.model_file)
    ncdf_model_file = root + '.ncdf'
    logging.info('ncdf model_file %s' % ncdf_model_file)
    scope.output.write('  File: %s\n' % ncdf_model_file)

    write_boolean(scope, 'Tolerant')
    scope.output.write('\n')
    write_boundarycondition(scope)
    write_materials(scope)

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_boundarycondition(scope):
    '''Writes SurfaceProperty attributes to output stream

    '''
    print('Write boundary conditions')
    atts = scope.sim_atts.findAttributes('SurfaceProperty')
    if not atts:
        return
    atts.sort(key=lambda att:att.name())

    scope.output.write('  BoundaryCondition: {\n')

    # First write HFormulation if item is enabled
    write_boolean(scope, 'HFormulation', indent='    ')

    # Traverse attributes and write BoundaryCondition contents
    surface_material_list = list()  # for saving SurfaceMaterial info
    for att in atts:
        ent_string = utils.format_entity_string(scope, att)
        if not ent_string:
            continue  # warning?

        att_type = att.type()

        # Periodic BC is special case
        if att_type == 'Periodic':
            if scope.solver_list[0] == 'S3P':
                raise Exception('ERROR: Specified Periodic surface with S3P analysis -- invalid')

            # Write slave item
            scope.output.write('    Periodic_S: %s\n' % ent_string)

            # Write master item
            master_item = att.findComponent('MasterSurface')
            ent = master_item.value(0)
            if ent is not None:
                prop_idlist = scope.model_resource.integerProperty(ent.id(), 'pedigree id')
                if prop_idlist:
                    scope.output.write('    Periodic_M: %s\n' % prop_idlist[0])
            else:
                scope.warning_messages.append('Warning: no slave surface specified for Periodic BC')

            # Write relative phase angle
            phase_item = att.findDouble('Theta')
            phase = phase_item.value(0)
            scope.output.write('    Theta: %f\n' % phase)

            # This completes Periodic case
            continue

        scope.output.write('    %s: %s\n' % (att_type, ent_string))

        # Check for sigma and frequency items
        added_text = None
        sigma_item = att.findDouble('Sigma')
        if sigma_item is not None and utils.passes_categories(sigma_item, scope.categories):
            sigma = sigma_item.value(0)
            line1 = '    ReferenceNumber: %s\n' % ent_string
            line2 = '    Sigma: %g\n' % sigma
            added_text = line1 + line2

            # Frequency only set when Sigma is set (T3P Impedance)
            freq_item = att.findDouble('Frequency')
            if freq_item is not None and utils.passes_categories(freq_item, scope.categories):
                freq = freq_item.value(0)
                line3 = '    Frequency: %d\n' % freq
                added_text += line3

        if added_text:
            surface_material_list.append(added_text)
    scope.output.write('  }\n')

    # Traverse surface_material_list and write SurfaceMaterial entries
    for surface_material_string in surface_material_list:
        scope.output.write('\n')
        scope.output.write('  SurfaceMaterial: {\n')
        scope.output.write(surface_material_string)
        scope.output.write('  }\n')

# ---------------------------------------------------------------------
def write_boolean(scope, att_type, item_name=None, output_name=None, indent='  '):
    '''Writes boolean property if item is checked

    Attribute should be a singleton
    '''
    #print 'write_boolean', att_type
    atts = scope.sim_atts.findAttributes(att_type)
    if not atts:
        return

    # (else)
    att = atts[0]
    if not item_name:
        item_name = att_type
    item = att.findVoid(item_name)

    if not output_name:
        output_name = att_type
    if item and item.isEnabled():
        scope.output.write('%s%s: 1\n' % (indent, output_name))

# ---------------------------------------------------------------------
def write_materials(scope):
    '''Writes Material attributes to output stream

    '''
    print('Write materials')
    atts = scope.sim_atts.findAttributes('Material')
    if not atts:
        return
    atts.sort(key=lambda att:att.name())

    # Traverse attributes
    for att in atts:
        ent_string = utils.format_entity_string(scope, att)
        if not ent_string:
            continue  # warning?

        scope.output.write('\n')
        scope.output.write('  Material: {\n')
        scope.output.write('    Attribute: %s\n' % ent_string)

        # Make list of (item name, output label) to write
        items_todo = [
            ('Epsilon', 'Epsilon'),
            ('Mu', 'Mu'),
            ('ImgEpsilon', 'EpsilonImag'),
            ('ImgMu', 'MuImag')
        ]
        for item_info in items_todo:
            name, label = item_info
            item = att.findDouble(name)
            if item and item.isEnabled() and utils.passes_categories(item, scope.categories):
                value = item.value(0)
                scope.output.write('    %s: %g\n' % (label, value))

        scope.output.write('  }\n')


# ---------------------------------------------------------------------
def write_finiteelement(scope):
    '''Writes FiniteElement section to output stream

    '''
    print('Write FiniteElement')
    scope.output.write('\n')
    scope.output.write('FiniteElement:\n')
    scope.output.write('{\n')

    att = scope.sim_atts.findAttributes('FEInfo')[0]

    order_item = att.findInt('Order')
    scope.output.write('  Order: %d\n' % order_item.value(0))

    curved_surfaces = 'off'
    curved_item = att.findVoid('EnableCurvedSurfaces')
    if curved_item and curved_item.isEnabled():
        curved_surfaces = 'on'
    scope.output.write('  CurvedSurfaces: %s\n' % curved_surfaces)

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_pregion(scope):
    '''Writes PRegion section to output stream

    '''
    print('Write PRegion')
    atts = scope.sim_atts.findAttributes('RegionHighOrder')
    if not atts:
        return
    atts.sort(key=lambda att:att.name())

    # Traverse attributes
    for att in atts:
        ent_string = utils.format_entity_string(scope, att)
        if not ent_string:
            continue  # warning?

        order_item = att.findInt('RegionHighOrder')
        order = order_item.value(0)

        scope.output.write('\n')
        scope.output.write('PRegion:\n')
        scope.output.write('{\n')
        scope.output.write('  Type: Material\n')
        scope.output.write('  Reference: %s\n' % ent_string)
        scope.output.write('  Order: %d\n' % order)
        scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_eigensolver(scope):
    '''Writes Omega3P EigenSolver section to output stream

    '''
    print('Write EigenSolver')
    freq_att = scope.sim_atts.findAttributes('FrequencyInfo')[0]
    scope.output.write('\n')
    scope.output.write('EigenSolver:\n')
    scope.output.write('{\n')

    for name in ['NumEigenvalues', 'FrequencyShift']:
        item = freq_att.find(name)
        assert item is not None, 'no item {}'.format(name)
        scope.output.write('  %s: %g\n' % (name, item.value(0)))

    solver_att = scope.sim_atts.findAttribute('Omega3PSolver')
    tol_item = solver_att.find('Tolerance')
    if tol_item is not None:
        scope.output.write('  %s: %g\n' % ('Tolerance', tol_item.value(0)))

    mode_item = solver_att.findString('Mode')
    if mode_item.value() == 'cork':
        scope.output.write('  SolverMode: CORK\n')
        scope.output.write('  CORK: {\n')

        n = mode_item.numberOfActiveChildrenItems()
        for i in range(n):
            item = mode_item._activeChildItem(i)
            scope.output.write('    {}: {:g}\n'.format(item.name(), item.value()))
        scope.output.write('  }\n')

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_frequency_scan(scope):
    '''Writes S3P WaveguideFrequency line or FrequencyScan section

    '''
    att = scope.sim_atts.findAttributes('FrequencyInfo')[0]

    # Added single WaveguideFrequency March 2020
    mode = 'scan'  # for backward compatibility

    mode_item = att.findString('FrequencyMode')
    if mode_item is not None:
        mode = mode_item.value()

    if mode == 'waveguide':
        freq_item = att.itemAtPath('FrequencyMode/WaveguideFrequency')
        if freq_item is None:
            raise RuntimeError('FrequencyMode/WaveguideFrequency item not found')
        print('Write WaveguideFrequency')
        scope.output.write('\nWaveguideFrequency: %g\n' % freq_item.value(0))
        return

    print('Write FrequencyScan')
    scope.output.write('\n')
    scope.output.write('FrequencyScan:\n')
    scope.output.write('{\n')

    # Make list of (smtk_item_path, s3p_file_keyword)
    prefix = '' if mode_item is None else 'FrequencyMode/'
    item_spec = [
        (prefix + 'StartingFrequency', 'Start'),
        (prefix + 'EndingFrequency', 'End'),
        (prefix + 'FrequencyInterval','Interval')]
    for item_path,s3p_keyword in item_spec:
        item = att.itemAtPath(item_path)
        if item is None:
            raise RuntimeError('Item {} not found'.format(item_path))
        scope.output.write('  %s: %g\n' % (s3p_keyword, item.value(0)))

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_moving_window(scope):
    '''Writes PRegion w/AutomaticMovingWindow for wakefield analyses

    '''
    att_list = scope.sim_atts.findAttributes('MovingWindow')
    if not att_list:  # older version
        return

    att = att_list[0]
    group_item = att.findGroup('MovingWindow')
    if group_item is None or not group_item.isEnabled():
        return

    print('Write AutomaticMovingWindow')
    scope.output.write('\n')
    scope.output.write('PRegion:\n')
    scope.output.write('{\n')

    scope.output.write('  Type: AutomaticMovingWindow\n')
    order_item = group_item.find('Order')
    scope.output.write('  Order: %d\n' % order_item.value(0))
    back_item = group_item.find('Back')
    scope.output.write('  Back: %s\n' % back_item.value(0))
    front_item = group_item.find('Front')
    scope.output.write('  Front: %s\n' % front_item.value(0))
    end_item = group_item.find('StructureEnd')
    scope.output.write('  StructureEnd: %s\n' % end_item.value(0))

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_loading_info(scope):
    '''Writes LoadingInfo sections for T3P analysis

    '''
    att_list = scope.sim_atts.findAttributes('BeamLoading')
    if not att_list:
        return

    print('Write LoadingInfo')
    for att in att_list:
        scope.output.write('\n')
        scope.output.write('LoadingInfo:\n')
        scope.output.write('{\n')

        # Write the Bunch subsection
        scope.output.write('\n')
        scope.output.write('  Bunch:\n')
        scope.output.write('  {\n')

        bunch_type = att.findString('Bunch Type').value(0)
        if bunch_type == 'Gaussian':
            scope.output.write('  Type: Gaussian\n')

            sigma = att.findDouble('Sigma').value(0)
            scope.output.write('  Sigma: %f\n' % sigma)
        elif bunch_type == 'Bi-Gaussian':
            scope.output.write('  Type: BiGaussian\n')

            sigma = att.findDouble('Sigma1').value(0)
            scope.output.write('  Sigma1: %f\n' % sigma)

            sigma = att.findDouble('Sigma2').value(0)
            scope.output.write('  Sigma2: %f\n' % sigma)
        else:
            msg = 'Unrecognized Bunch Type "{}"'.format(bunch_type)
            scope.warning_messages.append(msg)
            scope.output.write('}\n')
            continue

        # Common Bunch parameters
        num_sigmas = att.findInt('numSigmas').value(0)
        scope.output.write('  Number of sigmas: %d\n' % num_sigmas)

        charge = att.findDouble('Charge').value(0)
        scope.output.write('  Charge: %g\n' % charge)

        # Finish Bunch subsection
        scope.output.write('  }\n')

        # Rest of LoadingInfo

        # SymmetryFactor
        symmetry_factor = att.findDouble('SymmetryFactor').value(0)
        scope.output.write('  SymmetryFactor: %s\n' % symmetry_factor)

        # Start point
        start_point_item = att.findDouble('StartPoint')
        start_point_string = utils.format_vector(start_point_item, '%g')
        scope.output.write('  StartPoint: %s\n' % start_point_string)

        # Todo Direction
        direction_item = att.findDouble('Direction')
        direction_string = utils.format_vector(direction_item, '%g')
        scope.output.write('  Direction: %s\n' % direction_string)

        # Source boundary
        boundary_item = att.findComponent('SourceBoundary')
        boundary_ids = utils.get_entity_ids(scope, boundary_item)
        if len(boundary_ids) == 0:
            msg = 'No SourceBoundary specified for LoadingInfo {}'.format(att.name())
            scope.warning_messages.append(msg)
        else:
            scope.output.write('  BoundaryID: %d\n' % boundary_ids[0])

        # Finish LoadingInfo
        scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_time_stepping(scope):
    '''Writes TimeStepping section for T3P analysis

    '''
    print('Write TimeStepping')

    att_list = scope.sim_atts.findAttributes('FEInfo')
    if not att_list:
        print('Missing FEInfo attribute')
        return
    feinfo_att = att_list[0]

    scope.output.write('\n')
    scope.output.write('TimeStepping:\n')
    scope.output.write('{\n')

    max_time = feinfo_att.findDouble('MaxTime').value(0)
    scope.output.write('  MaximumTime: %g\n' % max_time)

    dt = feinfo_att.findDouble('DT').value(0)
    scope.output.write('  DT: %g\n' % dt)

    scope.output.write('}\n')


# ---------------------------------------------------------------------
def write_monitor(scope):
    '''Writes Monitor sections for T3P analysis

    '''
    print('Write Monitor')
    att_list = scope.sim_atts.findAttributes('Monitor')
    if not att_list:
        return

    # Map attribute type to T3P keyword
    type_map = {
        'ModeVoltageMonitor': 'ModeVoltage',
        'PointMonitor': 'Point',
        'PowerMonitor': 'Power',
        'VolumeMonitor': 'Volume',
        'WakeFieldMonitor': 'WakeField'
    }

    for att in att_list:
        scope.output.write('\n')
        scope.output.write('Monitor:\n')
        scope.output.write('{\n')

        type_string = type_map.get(att.type())
        if type_string is None:
            logging.warning('Unknown monitor type %s' % att.type())
            type_string = att.type()
        scope.output.write('  Type: %s\n' % type_string)
        # Name current found as name item instead of att.name()
        name_item = att.findString('Name')
        scope.output.write('  Name: %s\n' % name_item.value(0))

        # Voltage Monitor a special case
        if type_string == 'ModeVoltage':
            write_mode_voltage_monitor(scope, att)
            scope.output.write('}\n')
            continue

        item_type_list = \
            ['Point', 'TimeStart', 'TimeEnd', 'TimeStep', 'StartContour', 'EndContour', 'Smax']
        for item_type in item_type_list:
            item = att.find(item_type)
            if item is None or not utils.passes_categories(item, scope.categories):
                continue
            if not item.isEnabled():
                continue
            if not item.isSet():
                continue
            if item.numberOfValues() > 1:
                value_string = utils.format_vector(item)
                scope.output.write('  %s: %s\n' % (item_type, value_string))
            else:
                scope.output.write('  %s: %s\n' % (item_type, item.value(0)))

        # Handle component items as special case
        for name in ['MonitorBoundary', 'MonitorPort']:
            boundary_item = att.findComponent(name)
            if boundary_item is not None:
                idlist = utils.get_entity_ids(scope, boundary_item)
                scope.output.write('  ReferenceNumber: %d\n' % idlist[0])
        scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_mode_voltage_monitor(scope, att)        :
    '''Writes unique parts of Voltage Monitor section for T3P analysis

    * Only writes inner part of the section.
    * Type and Name should be written by calling code
    '''
    scope.output.write('  Port: {\n')

    model_entity_item = att.find('MonitorPort')
    ent_idlist = utils.get_entity_ids(scope, model_entity_item)
    if not ent_idlist:
        raise Exception('ModeVoltage specified without Port')
    scope.output.write('    ReferenceNumber: %d\n' % ent_idlist[0])

    group_item = att.find('ESolver')
    scope.output.write('    ESolver: {\n')

    # List of <item name, T3P keyword> pairs
    item_keyword_table = [
        ('Type', 'Type', '%s'),
        ('NumberOfModes', 'NumberOfModes', '%d'),
        ('Frequency', 'Frequency', '%g'),
        ('Tolerance', 'Tolerance', '%g'),
        ('MaxIterations', 'MaxIterations', '%d')
    ]
    for item_type,keyword,fmt in item_keyword_table:
        item = group_item.find(item_type)
        #scope.output.write('      %s: %s\n' % (keyword, item.value(0)))
        format_string = '      %s: %s\n' % (keyword, fmt)
        scope.output.write(format_string % item.value(0))
    scope.output.write('    }\n')

    scope.output.write('  }\n')

# ---------------------------------------------------------------------
def write_linear_solver(scope):
    '''Writes LinearSolver section for T3P analysis

    '''
    print('Write LinearSolver')

    att_list = scope.sim_atts.findAttributes('FEInfo')
    if not att_list:
        print('Missing FEInfo attribute')
        return
    feinfo_att = att_list[0]

    scope.output.write('\n')
    scope.output.write('LinearSolver:\n')
    scope.output.write('{\n')

    solver_item = feinfo_att.findString('LinearSolver')
    scope.output.write('  Solver: %s\n' % solver_item.value(0))

    # Check for optional preconditioner
    precondx_item = solver_item.findChild(
        'Preconditioner', smtk.attribute.ACTIVE_CHILDREN)
    if precondx_item is not None:
        scope.output.write('  Preconditioner: %s\n' % precondx_item.value(0))

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_port(scope):
    '''Writes Port sections for each surface of att_type Waveguide

    '''
    print('Write Port')
    atts = scope.sim_atts.findAttributes('Waveguide')
    if not atts:
        return
    atts.sort(key=lambda att:att.name())

    # Traverse attributes
    for att in atts:
        #print 'att', att.name()

        ent_idlist = utils.get_entity_ids(scope, att.associations())
        if not ent_idlist:
            print('WARNING: Waveguide %s not assigned to any model entities' % att.name())
            continue

        # Can only specify one ReferenceNumber per Port,
        # so write separate Port section for each model entity
        for ent_id in ent_idlist:
            mode_item = att.findInt('NumModes')
            num_modes = mode_item.value(0)

            scope.output.write('\n')
            scope.output.write('Port:\n')
            scope.output.write('{\n')
            scope.output.write('  ReferenceNumber: %s\n' % ent_id)
            scope.output.write('  NumberOfModes: %d\n' % num_modes)
            scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_postprocess(scope):
    '''Writes PostProcess section to output stream

    '''
    att_list = scope.sim_atts.findAttributes('PostProcess')
    if not att_list:
        return

    att = att_list[0]

    # Not all solvers use PostProcess
    if not utils.passes_categories(att, scope.categories):
        return

    print('Write PostProcess')
    scope.output.write('\n')
    scope.output.write('PostProcess:\n')
    scope.output.write('{\n')

    # Version 0
    item = att.find('ModeFiles')
    if item:
        toggle = 'on' if item.isEnabled() else 'off'
        scope.output.write('  Toggle: %s\n' % toggle)

    # Version 1 and up
    toggle_item = att.findGroup('Toggle')
    if toggle_item:
        toggle = 'on' if toggle_item.isEnabled() else 'off'
        scope.output.write('  Toggle: %s\n' % toggle)

        prefix_item = toggle_item.find('ModeFilePrefix')
        if prefix_item.isEnabled() and prefix_item.isSet(0):
            prefix = prefix_item.value(0)
            scope.output.write('  ModeFile: %s\n' % prefix)

        # Check all Waveguide attributes for Postprocess option
        waveguide_atts = scope.sim_atts.findAttributes('Waveguide')
        for waveguide_att in waveguide_atts:
            pp_item = waveguide_att.findVoid('Postprocess')
            if pp_item and pp_item.isEnabled():
                ent_string = utils.format_entity_string(scope, waveguide_att)
                scope.output.write('  PortNumber: %s\n' % ent_string)

    scope.output.write('}\n')

# ---------------------------------------------------------------------
def write_checkpoint(scope):
    """Writes CheckPoint section to output stream (T3P)."""
    if not 'T3P' in scope.categories:
        return

    checkpoint_att = scope.sim_atts.findAttribute('CheckPoint')
    if checkpoint_att is None:
        raise RuntimeError('Unable to find CheckPoint attribute')

    print('Write CheckPoint')
    scope.output.write('\n')
    scope.output.write('CheckPoint:\n')
    scope.output.write('{\n')

    action_item = checkpoint_att.findString('Action')
    if action_item is not None and action_item.isSet():
        scope.output.write('  Action: {}\n'.format(action_item.value()))
    interval_item = action_item.findChild('Ntimesteps', smtk.attribute.SearchStyle.ACTIVE_CHILDREN)
    if interval_item is not None and interval_item.isSet():
        scope.output.write('  Ntimesteps: {}\n'.format(interval_item.value()))

    scope.output.write('}\n')
