package bilab;

import java.io.*;
import java.util.*;



public class tree implements IAnnotated, IUserText, IResourceIOProvider
{
  @Summary("create a tree with no children")
  public tree()
  {
    _children = new scigol.List();
    _annotations = new scigol.Map();
    name = "<tree>";
  }
  
  @Summary("create a new tree from a Newick formatted string")
  @scigol.signature("explicit func(newickString:string -> self)")
  public tree(String newickString)
  {
    tree t = parseNewick(newickString);
    _children = t._children;
    _annotations = t._annotations;
  }
  
  
  @scigol.accessor
  public scigol.List get_children()
  {
    return _children;
  }
  
  
  @scigol.accessor
  public void set_children(scigol.List children)
  {
    for(int c=0; c<children.get_size(); c++) {
      if (!(children.get_Item(c).value instanceof tree))
        throw new BilabException("children of a tree must also be trees");
    }
    this._children = children;
  }
  
  
  
  public void addChild(Object child)
  {
    _children.add(child);
  }
  
  
  
  
  
  public scigol.Map get_annotations()
  {
    return _annotations;
  }
  
  private static final java.util.List<String> supportedImportResourceTypes;
  private static final java.util.List<String> supportedExportResourceTypes;
  
  static {
    // list of supported resource name type (not extensions)
    supportedImportResourceTypes = new LinkedList<String>();
    supportedImportResourceTypes.add("Newick");
    
    supportedExportResourceTypes = new LinkedList<String>();
    supportedExportResourceTypes.add("Newick");
    supportedExportResourceTypes.add("list(Newick)");
  }
  
  
  public static java.util.List<String> getSupportedImportResourceTypes()
  {
    return supportedImportResourceTypes;
  }

  public static java.util.List<String> getSupportedExportResourceTypes()
  {
    return supportedExportResourceTypes;
  }

  
  
  @Sophistication(Sophistication.Advanced)
  public static Object importResource(ResourceManager rm, String resourceName, String resourceType)
  {
    try {
      InputStream inputStream = rm.findResourceStream(resourceName);
      if (inputStream == null)
        throw new BilabException("unable to open resource:"+resourceName);

      // just read the whole stream into a string first
      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
      int b=0;
      while ((b = inputStream.read()) != -1) 
        bytes.write(b);
      
      String newickString = bytes.toString();

      inputStream.close();
      
      tree t = parseNewick(newickString);
      t.name = Util.name(resourceName);
      
      return t;
      
    } catch (BilabException e) {
      throw e;
    } catch (Exception e) {
      throw new BilabException("unable to locate/import resource as tree: "+resourceName+" - "+e);
    }
  }

  
  
  @Summary("create a resource containing data in a supported format from a seq")
  public static void exportResource(ResourceManager rm, Object o, String resourceName, String resourceType)
  {
    try {
      if (resourceType.equals("Newick")) { 
        if (!(o instanceof tree))
          Notify.devError(tree.class,"object for export of resource type Newick must be a tree");
        
        OutputStream outStream = rm.createResourceStream(resourceName);
        tree t = (tree)o;
        String newickString = t.get_Newick();
        
        outStream.write(newickString.getBytes());
        
        outStream.flush();
        outStream.close();
      }
      else if (resourceType.equals("list(Newick)")) {
        
        if (!(o instanceof List))
          Notify.devError(tree.class,"object for export of resource type list(Newick) must be a list");
        
        OutputStream outStream = rm.createResourceStream(resourceName);
        List l = (List)o;
        
        for(int ti=0; ti<l.size(); ti++) {
          Object e = scigol.TypeSpec.unwrapAny( l.get(ti) );
          if (!(e instanceof tree))
            throw new BilabException("list must only contain elements of type 'tree'");
          tree t = (tree)e;
          String newickString = t.get_Newick()+"\r\n";
        
          outStream.write(newickString.getBytes());
        }
        
        outStream.flush();
        outStream.close();
        
      }
      else
        throw new BilabException("unsupported resource type:"+resourceType);
      
    } catch (Exception e) {
      throw new BilabException("unable to export tree as resource type: "+resourceType+" - "+e.getMessage());
    }
  }
  
  
  
  
  
  
  // helpers for parsing Newick format
  protected static class ParseState { String s; int p; }
  
  protected static char nextChar(ParseState ps)
  {
    if (ps.p >= ps.s.length()) return (char)0;
    
    // skip spaces & newlines
    while (    (ps.s.charAt(ps.p) == ' ') 
            || (ps.s.charAt(ps.p) == '\n') || (ps.s.charAt(ps.p) == '\r'))
      ps.p++;

    return ps.s.charAt(ps.p++);
  }
  
  protected static char peekChar(ParseState ps)
  {
    if (ps.p >= ps.s.length()) return (char)0;
      
    // skip spaces & newlines
    while (    (ps.s.charAt(ps.p) == ' ') 
            || (ps.s.charAt(ps.p) == '\n') || (ps.s.charAt(ps.p) == '\r'))
      ps.p++;

    return ps.s.charAt(ps.p);
  }
  
  
  protected static tree parseNewick(String s)
  {
    ParseState ps = new ParseState();
    ps.s = s;
    ps.p = 0;
    tree t;
    if (peekChar(ps) == '(') 
      t = parseTree(ps);
    else 
      t = parseLeaf(ps);
    
    // terminator (be generous and don't complain if the terminator is missing
    //  if we're at the end of the input)
    char nextChar = nextChar(ps);
    if ((nextChar != ';') && (nextChar != (char)0)) 
      throw new BilabException("Newick format tree must end in a ';'");
    
    return t;
  }
  
  protected static tree parseLeaf(ParseState ps)
  {
    // name [':' branch-length]
    String name = parseName(ps);
    tree leaf = new tree(); // no children
    leaf._annotations.add("name",name.replace('_',' '));
    
    if (peekChar(ps) == ':') {
      nextChar(ps);
      double len = parseLength(ps);
      leaf._annotations.add("length",len);
    }
    
    return leaf;
  }
  
  protected static tree parseTree(ParseState ps)
  {
    // assume a tree, then if only one node, just return it alone
    tree t = new tree();
    
    if (nextChar(ps) != '(')
      throw new BilabException("expected '(' in Newick tree format");
    
    tree node;
    char lastChar;
    
    do {
      if (peekChar(ps) != '(')  // leaf
        node = parseLeaf(ps);
      else {
        node = parseTree(ps);

        if ((peekChar(ps) != ',') && (peekChar(ps) != ':')) {
          String name = parseName(ps);
          node._annotations.add("name",name.replace('_',' '));
        }
        if (peekChar(ps) == ':') { // branch length
          nextChar(ps); // eat ':'
          double len = parseLength(ps);
          node._annotations.add("length",len);
        }
      }
    
      t._children.add(node);
      
      lastChar = nextChar(ps);

    } while (lastChar == ',');

    if (lastChar != ')')
      throw new BilabException("expected ')' in Newick tree format");
    
    return t;
  }
  
  protected static String parseName(ParseState ps)
  {
    String name = "";
    while (    (peekChar(ps) != ':') && (peekChar(ps) != ',') 
            && (peekChar(ps) != ')') && (peekChar(ps) != ';')
            && (peekChar(ps) != (char)0)) {
      name += nextChar(ps);
    }
    return name;
  }
  
  
  protected static double parseLength(ParseState ps)
  {
    double len = 0;
    double m = 1;
    boolean fraction = false;
    while (Character.isDigit(peekChar(ps)) || (!fraction && peekChar(ps)=='.')) {
      char c = nextChar(ps);
      if (c != '.') {
        if (!fraction) 
          len = 10.0*len + Character.getNumericValue(c);
        else {
          m *= 0.1;
          len += m*Character.getNumericValue(c);
        }
      }
      else
        fraction = true;
    }
    
    return len;
  }
  
  
  // helpers for Newick output
  protected static void toNewickString(tree t, StringBuilder sb)
  {
    if (t._children.get_size() > 0) sb.append("(");
    for(int c=0; c<t._children.get_size();c++) {
      toNewickString( (tree)t._children.get_Item(c).value, sb);
      if (c != t._children.get_size()-1)
        sb.append(", ");
    }
    if (t._children.get_size() > 0) sb.append(")");
    
    String name = (String)t._annotations.get_Item("name").value;
    if (name == null) name = "";
    sb.append(name.replace(' ','_'));
    Double len = (Double)t._annotations.get_Item("length").value;
    if (len != null) {
      double d = len.doubleValue();
      if (d > 0.00001) // round
        d = ((int)(100000.0*d))/100000.0;
      sb.append(":"+d);
    }
  }
  
  
  @scigol.accessor
  public String get_Newick()
  {
    StringBuilder sb = new StringBuilder();
    toNewickString(this,sb);
    return sb.toString()+";";
  }
  
  
  public String toString()
  {
    return get_Newick();
  }
  
  
  public String get_ShortText()
  {
    return name;
  }
  
  
  public String get_DetailText()
  {
    return "";
  }
  
  private String name;
  private scigol.List _children; // list of children (trees or Objects)
  private scigol.Map _annotations;
  
}
