#!/bin/sh
''':'
# This code will be treated as a comment in Python, but will be executed
# by the shell, allowing us to do some special handling before the python
# interpreter starts.
PYTHONPATH="" exec python3 "$0" "$@"
' '''

version_str="0.1.6"

# 2025-08-13  0.1.6
#             Fixed a bug where backslashes at the end of a line weren't
#             being treated as a way to escape the newline.
# 2024-06-29  0.1.5
#             Fixed a bug where -getfile would try to return whole sections.
# 2023-11-13  0.1.4
#             Now ignores PYTHONPATH
# 2022-08-22  Switch to using python3
# 2021-12-23  Fixed a crash if no workspace was set.
# 2021-07-01  0.1.3
#             Fixed some Python3 compatibility issues.
#             Improved handling of missing GIANT_DATA_DIR or BIO_DATA_DIR
#             environment variables.
# 2019-03-25  0.1.2
#             Fixed a crash if a non-existing input file was given.
#             Fixed a crash if there was a close brace while not in a section.
#             Improved the usage info.
# 2017-07-06: Fixed a bug introduced in the previous version.
# 2017-06-30: Fixed a crash if you tried to get a sub-tag from a non-tag
#             section.
# 2016-04-27: Now defaults to using GIANT_DATA_DIR if it is set.
# 2016-02-01: 0.1.1
# 2016-02-01: The -v option was printing an old version number.
#             Added information about referencing tags by instance number to
#             the usage.
# 2014-12-17: Updated to handle GIANT_DATA_DIR environment variable.
# 2011-06-03: 0.1.0
#             Change to the #!, which doesn't require a specific python
#             executable directory.
#             Slight modification to allow script to be run with older versions
#             of python.
# 2010-06-28: 0.0.3
#             Now preserves the tag order when multiple instances of multiple
#             tags appear in the same section
# 2009-10-13: 0.0.2
#             Now preserves indentation on non-tagged sections.
# 2008-07-16: 0.0.1
#             Added -v option.
# 2008-07-16: Made it return multiple values if there were multiple matching
#             tags instead of just returning the first one.

import sys
import string
import os

stderr=sys.stderr

data_dir_var='GIANT_DATA_DIR'
data_dir=os.environ.get(data_dir_var)

if data_dir is None:
  data_dir_var='BIO_DATA_DIR'
  data_dir=os.environ.get(data_dir_var)


# Return (0,tag) if value is a colon-value
# Return (1,tag) for the start of a section
# Return (2,text) for a single text line
# Return (-1,tag) for the end of a section
# Return (-2,value) for a blank line
def parse_line(s):
  index=0
  slen=len(s)
  while index<slen:
    c=s[index]
    if c!=' ':
      break
    index=index+1
  start=index
  if index==slen:
    return (-2,)
  if index<slen and s[index]=='}':
    return (-1,)
  while index<slen:
    c=s[index]
    if c==' ' or c==':':
      break
    index=index+1
  end=index
  tag=s[start:end]
  if index==slen:
    return (2,tag)
  if s[index]==':':
    index=index+1
    while index<slen:
      if s[index]!=' ':
        break
      index=index+1
    value=s[index:]
    return (0,tag,value)
  while index<slen:
    if s[index] == '{':
      return (1, tag)
    if s[index] not in [' ', '\t']:
      break;
    index=index+1

  return (2,s[start:])

class Writer:
  def __init__(self,port):
    self.indent_level=0
    self.port=port

  def write_section(self,values):
    tag_order_tag='_TAG_ORDER_'
    if tag_order_tag in values.keys():
      tag_order=values[tag_order_tag]
    else:
      tag_order=[]
    for (tagbase,tagnum) in tag_order:
      entry=values[tagbase][tagnum]
      if tagbase == '__TEXT__':
        for line in entry.split('\n'):
          self.indent()
          self.write(line+'\n')
      else:
        self.write_value(tagbase,entry)

  def write(self,str):
    self.port.write(str)

  def indent(self):
    for i in range(0,self.indent_level):
      self.write("  ")

  def adj_indent(self,offset):
    self.indent_level=self.indent_level+offset

  def write_value(self,tag,value):
    self.indent()
    if type(value)==type({}):
      self.write(tag+" {\n")
      self.adj_indent(1)
      self.write_section(value)
      self.adj_indent(-1)
      self.indent()
      self.write("}\n")
    else:
      self.write(tag+": "+value+"\n")


def readLinesFrom(file):
  lines = file.read().split('\n')

  i = 0

  while i < len(lines):
    if lines[i].endswith('\\'):
      if i + 1 == len(lines):
        # No newline at end of file.  I guess we should just ignore this
        pass
      else:
        lines[i] = lines[i] + '\n' + lines[i+1]
        del lines[i+1]
    else:
      i += 1

  return lines


class TagFileData:
  def __init__(self,typename=None,version=None,values={}):
    self.values=values
    self.typename=None
    self.version=None

  def __read(self, lines, readHeader=1):
    if readHeader:
      if len(lines)<2:
        raise RuntimeError('File is not a valid tag file')
      self.typename=lines[0]
      self.version=lines[1]
      lines=lines[2:]
    else:
      self.typename=None
      self.version=None

    stack=[{}]

    for line in lines:
      result = parse_line(line)
      tag = None

      if result[0]==2:
        # Single text line
        tag = '__TEXT__'
        value = result[1]
        current = stack[-1]

        if tag in current.keys():
          current[tag].append(value)
        else:
          current[tag] = [value];
      elif result[0]==1:
        # Section start
        tag=result[1]
        newsection={}
        current=stack[-1]

        if tag in current:
          current[tag].append(newsection)
        else:
          current[tag]=[newsection]

        stack.append(newsection)
      elif result[0]==0:
        # Colon value
        tag = result[1]
        value = result[2]
        current = stack[-1]

        if tag in current:
          current[tag].append(value)
        else:
          current[tag] = [value]
      elif result[0]==-1:
        # Section end

        if len(stack)==1:
          stderr.write('Close brace encountered while not in a section.\n')
          sys.exit(1)

        stack=stack[0:-1]
      if tag!=None:
        tag_order_tag='_TAG_ORDER_'
        if not tag_order_tag in current.keys():
          current[tag_order_tag]=[]
        tagnum=len(current[tag])-1
        current[tag_order_tag].append((tag,tagnum))
    self.values=stack[0]

  def readStr(self, str, readHeader=0):
    lines = str.split('\n')
    self.__read(lines, readHeader)

  def read(self,path,readHeader=1):
    try:
      file = open(path,'r')
    except IOError as e:
      stderr.write('Unable to open %s: %s\n'%(path,e.strerror))
      sys.exit(1)

    lines = readLinesFrom(file)
    file.close()
    self.__read(lines, readHeader)

  def value(self,tag,converter):
    if not tag in self.values.keys():
      return None
    else:
      return converter(self.values[tag])

  def floatValue(self,tag):
    return self.value(tag,string.atof)

  def pathValue(self,tag):
    return self.value(tag,os.path.expandvars)

  def strValue(self,tag):
    return self.values[tag]

  def set(self,tag,value):
    self.values[tag]=value

  def save(self,path):
    file=open(path,'w')
    self.write(file)
    file.close()

  def writeHeader(self,port):
    Writer(port).write(self.typename+'\n')
    Writer(port).write(self.version+'\n')

  def write(self,port):
    if self.typename!=None:
      self.writeHeader(port)
    Writer(port).write_section(self.values)

def readFile(path,readHeader=1):
  data=TagFileData()
  data.read(path,readHeader=readHeader)
  return data

argv=sys.argv
argc=len(sys.argv)
if argc<2:
  print('')
  print('tagfile '+version_str)
  print('')
  print('Usage: tagfile [<operation> ...] <infile> [<outfile>]')
  print('')
  print('Operations:')
  print('  -set <tag> <value>')
  print('  -setfile <tag> <value>')
  print('  -get <tag>')
  print('  -getfile <tag>')
  print('')
  print('The Nth occurrence of a tag can be referenced by concatenating a number to the')
  print('tag name, for example SUBJECT1 refers to the first SUBJECT tag and SUBJECT2')
  print('refers to the second SUBJECT tag.')
  print()
  print('Nested sections are handled using a period between tags.  For example,')
  print('OUTER.INNER means the INNER tag within the OUTER section.')
  print('')
  sys.exit(1)

if data_dir is None:
  stderr.write('GIANT_DATA_DIR is not set.\n')
  sys.exit(1)

if os.environ.get('GIANT_DATA_DIR') is None:
  os.environ['GIANT_DATA_DIR'] = data_dir

if os.environ.get('BIO_DATA_DIR') is None:
  os.environ['BIO_DATA_DIR'] = data_dir

class ArgParse:
  def __init__(self):
    self.argv=sys.argv
    self.argc=len(self.argv)
    self.argnum=1

  def atEnd(self):
    return self.argnum>=self.argc

  def current(self,desc):
    if self.atEnd():
      stderr.write('No value specified for '+desc+'\n')
      sys.exit(1)
    return self.argv[self.argnum]

  def next(self,desc):
    val=self.current(desc)
    self.inc()
    return val

  def inc(self):
    self.argnum=self.argnum+1

def tagNum(tag):
  lastdigits=''
  while tag[-1] in string.digits:
    lastdigits=tag[-1]+lastdigits
    tag=tag[0:-1]
  if lastdigits=='':
    tagnum=0
  else:
    tagnum=int(lastdigits)
  return (tag,tagnum-1)

def setList(l,index,v):
  while index>=len(l):
    l.append({})
  l[index]=v

# Put an empty dict as the value for the tag if the tag isn't
# already there.
def addTag(values,tagbase,tagnum):
  assert type(values)==type({})
  if values=={}:
    values['_TAG_ORDER_']=[]
  if tagbase not in values.keys():
    values[tagbase]=[]
  if len(values[tagbase])<=tagnum:
    setList(values[tagbase],tagnum,{})
    values['_TAG_ORDER_'].append((tagbase,tagnum))

# Remove the given empty valued tag
def removeTag(values,tagbase,tagnum):
  assert type(values[tagbase])==type([])
  while len(values[tagbase])>0 and values[tagbase][-1]=={}:
    del values[tagbase][-1]
  if len(values[tagbase])==0:
    del values[tagbase]
    values['_TAG_ORDER_'].remove((tagbase,tagnum))
    if len(values['_TAG_ORDER_'])==0:
      del values['_TAG_ORDER_']
      assert values=={}

def setTaglist(values,taglist,value):
  assert value!=''
  (tagbase,tagnum)=tagNum(taglist[0])
  if tagnum<0:
    tagnum=0
  addTag(values,tagbase,tagnum)
  if len(taglist)==1:
    values[tagbase][tagnum]=value
  else:
    if type(values[tagbase][tagnum])==type(''):
      values[tagbase][tagnum]={'__TEXT__':[values[tagbase][tagnum]],'_TAG_ORDER_':[]}
    setTaglist(values[tagbase][tagnum],taglist[1:],value)

def clearTaglist(values,taglist):
  (tagbase,tagnum)=tagNum(taglist[0])
  if tagnum<0:
    tagnum=0
  if tagbase in values:
    if tagnum<len(values[tagbase]):
      if len(taglist)>1:
        clearTaglist(values[tagbase][tagnum],taglist[1:])
      else:
        values[tagbase][tagnum]={}
      if values[tagbase][tagnum]=={}:
        removeTag(values,tagbase,tagnum)

def subpath(path,prefix,newprefix):
  if path[0:len(prefix)]!=prefix:
    return path
  return newprefix+path[len(prefix):]


def subdatadir(path):
  if data_dir is None:
    stderr.write('%s is not set\n'%(data_dir_var,))
    return path

  return subpath(path,data_dir,'$'+data_dir_var)


def setTag(data,tag,value):
  taglist=tag.split('.')

  if value=='':
    clearTaglist(data.values,taglist)
  else:
    setTaglist(data.values,taglist,value)


def setFileTag(data,tag,value):
  setTag(data,tag,subdatadir(value))


def dictTagValues(values, taglist):
  # taglist is the path of tags. If it is an empty list, that means
  # our values is the final value that we want.

  if taglist==[]:
    if type(values)==type([]):
      return values
    else:
      return [values]

  if type(values) is not type({}):
    return []

  tag=taglist[0]
  (tagbase,tagnum)=tagNum(tag)

  if tagbase not in values.keys():
    return []

  if type(values[tagbase])==type([]):
    if tagnum<0:
      result=[]
      for value in values[tagbase]:
        result.extend(dictTagValues(value,taglist[1:]))
      return result
    if tagnum>=len(values[tagbase]):
      return []
    return dictTagValues(values[tagbase][tagnum],taglist[1:])
  else:
    if tagnum!=0:
      return []
    return dictTagValues(values[tagbase],taglist[1:])


def tagValues(data, tag):
  taglist=tag.split('.')
  return dictTagValues(data.values, taglist)


def fileTagValues(data,tag):
  return [
    os.path.expandvars(value)
      for value in tagValues(data, tag)
        if isinstance(value, str)
  ]


class Operation:
  def __init__(self,opname,tag,value=None):
    self.name=opname
    self.tag=tag
    self.value=value


operations=[]
args=ArgParse()

while not args.atEnd():
  if args.current("op name")[0]!='-':
    break
  if args.current("version")=='-v':
    print('tagfile '+version_str)
    sys.exit(1)
  opname=args.next('op name')
  tag=args.next('tag')
  if opname=='-set' or opname=='-setfile':
    value=args.next('value')
  else:
    value=None
  operations.append(Operation(opname,tag,value))

infile=args.next("infile")

try:
  data=readFile(infile)
except RuntimeError as e:
  stderr.write('Error reading '+str(infile)+': '+str(e)+'\n')
  sys.exit(1)

if args.atEnd():
  outfile=None
else:
  outfile=args.next("outfile")


if not args.atEnd():
  print('Extra parameters given.')
  sys.exit(1)


for op in operations:
  if op.name=='-set':
    setTag(data, op.tag, op.value)
  elif op.name=='-setfile':
    setFileTag(data,op.tag,op.value)
  elif op.name=='-get':
    for value in tagValues(data, op.tag):
      print(value)
  elif op.name=='-getfile':
    for value in fileTagValues(data,op.tag):
      print(value)
  else:
    print('Unknown operation '+op.name)
    sys.exit(1)

if outfile!=None:
  data.save(outfile)
