def writeConstraintList(stream, constraintList):
  """ Write out a PALES (=NMRWISH?) format constraint list, 
  using only unambiguous, stereospecifically assigned constraints 
  Input: stream: A writable stream,  
  constraintList: CCPN ConstraintList, Distancem Hbond, Jcoupling, or RDC 
  """
  
  if constraintList.className not in ('DistanceConstraintList', 
                                      'HBondConstraintList', 
                                      'JCouplingConstraintList', 
                                      'RdcConstraintList'):
    raise Exception(
     "writeConstraintList: incorrect type of input constraint list: %s"
     % constraintList.className)
  
  # make data arrays, and get MolSystem
  iAtoms = []
  jAtoms = []
  constraints = []
  molSystems = set()
  for constraint in constraintList.sortedConstraints():
    items = constraint.sortedItems()
    if len(items) != 1:
      # skip ambiguous constraints
      continue
    item = items[0]
    
    resonances = item.orderedResonances or item.sortedResonances()
    assignAtoms = []
    for resonance in resonances:
      resonanceSet = resonance.resonanceSet
      if resonanceSet is not None:
        atomSets = list(resonanceSet.atomSets)
        if len(atomSets) == 1:
          atoms = list(atomSets[0].atoms)
          if len(atoms) == 1:
            assignAtoms.append(atoms)
    
    if len(assignAtoms) != 2:
      # only stereospecific assignments accepted
      continue
    
    if item.orderedResonances or atoms[0].name < atoms[1].name:
      # sort if not already ordered
      iAtom, jAtom = assignAtoms
    else:
      jAtom, iAtom = assignAtoms
    
    value = constraint.targetValue
    if value is None:
      continue
      
    iAtoms.append(iAtom)
    jAtoms.append(jAtom)
    constraints.append(constraint)
    
    molSystems.add(iAtom.topObject)
    molSystems.add(jAtom.topObject)
  
  # check for unique MolSystem
  if len(molSystems) == 1:
    molSystem = molSystems.pop()
  else:
    raise Exception(
     "writeConstraintList: input constraint list %s includes more than one MolSystem"
     % constraintList)
  
  # write header comment
  stream.write("# File generated by CcpNmr Analysis\n")
  stream.write("# CCPN %s %s\n" % (molSystem.qualifiedName, 
                                molSystem.getExpandedKey))
  stream.write("# CCPN %s %s\n" % (constraintList.qualifiedName, 
                                constraintList.getExpandedKey))
  
  # write sequence
  for chain in molSystem.chains:
    seqString = chain.molecule.seqString
    ll = []
    start = 0
    while start < len(seqString):
      # split seqString in chinks of 10
      ll.append(seqString[start:start+10])
      start += 10
      
    start = 0
    while start < len(ll):
      # write out list with five chunks per line
      stream.write('DATA SEQUENCE %s' % ' '.join(ll[start:start+5]))
      start += 5
  
  # write headers and format
  stream.write(
   "VARS   CHAIN_I RESID_I RESNAME_I ATOMNAME_I CHAIN_J RESID_J RESNAME_J ATOMNAME_J D DD W\n")
  format = "%3s %5d %6s %6s %3s %5d %6s %6s %9.3f %9.3f %.2f\n"
  stream.write("FORMAT %s" % format)
  stream.write('\n')
  
  # write actual data
  for ii, constraint in enumerate(constraints):
    ll = []
    for atom in (iAtoms[ii], jAtoms[ii]):
      residue = atom.residue
      chain = residue.chain
      chainCode = chain.pdbOneLetterCode
      if not chainCode.strip():
        chainCode = chain.code[0]
      ll.extend([chainCode, residue.seqCode, 
                 residue.chemCompVar.chemComp.code3Letter, atom.name])
    constraint
    ll.append(constraint.targetValue)
    ll.append(constraint.error)
    ll.append(constraint.weight)
    #
    stream.write(format % tuple(ll))


def parsePalesScript(text):
  """ Parse Pales script
  Input: Pales script (multiline string)
  Output: Keyword:value dictionary
  """
  
  result = {}
  
  # handle header comments
  lines = text.splitlines()
  for lineno,line in enumerate(lines):
    if line.startswith('# '):
      # Pales info comment
      ll = line[2:].split(':',1)
      if len(ll) == 2:
        # standard format - keyword:value
        key = ll[0].strip()
        val = ll[1].strip()
          
        if val and val[-1] == '.':
          val = val[:-1]
        
        if key.startswith('Refined '):
          # refined file lines. 
          # Format is: "# Refined ... ... rawfile: refinedfile.
          ll = key.split()
          key = ' '.join(ll[:-1])
          key2 = ' '.join(['Raw'] + ll[1:-1])
          result[key2] = ll[-1]
          
        result[key] = val
        
    elif line.strip():
      # neither comment nor empty. End of header comments
      break
    
  # get command string
  inCommand = False
  ll = []
  for line in lines[lineno:]:
    if line.startswith('/'):
      inCommand = True
    if inCommand:
      ss = line.strip()
      ll.append(ss)
      if ss[-1] == '\\':
        ll[-1] = ss[:-1]
      else:
        break
  
  result['command'] = ' '.join(ll)
  
  #
  return result
            
  
def parsePalesCommand(line):
  """ Parse Pales command
  Imput: Single line of form command -options ...
  Output: Dictionary of opt:val pairs. 
  Option names must start with '-'; 
  strings before the first option are returned as the value to key None
  
  """
  result = {}
  
  ll0 = line.split('-',1)
  command = ll0[0].strip() or None
  result[None] = command
  
  if len(ll0) > 1:
    ss = ll0[1].rstrip()
    ll = ss.split()
    lastOpt = ll[0]
    for ss in ll[1:]:
      if ss.startswith('-'):
        if lastOpt is not None:
          result[lastOpt] = None
        lastOpt = ss[1:]
      elif lastOpt is None:
        raise Exception("Option value %s not preceded by option name" % ss)
      else:
        result[lastOpt] = ss
        lastOpt = None
    else:
      if lastOpt is not None:
        result[lastOpt] = None
  #
  return result
        
