#!/usr/bin/env python3

import shutil
import argparse
import re
import os
from os import path
import subprocess
import json
import itertools
import glob
import sys
import logging

def print_model(model):
    main = model['main']

    assign = '='
    if main[0:1] == '~':
        main = main[1:]
        assign = '~'

    section = main + '\n'
    extracted = model['extracted']

    if extracted:
        section += '<table class="model">\n'
        for arg in extracted:
            name = arg[0]
            submodel = arg[1]
            (subassign, submodel) = print_model(submodel)
            section += '<tr><td>{}</td><td>{}</td><td>{}</td></tr>\n'.format(name,subassign,submodel)
        section += '</table>'
    return (assign, section)


def print_models(name,models):
    section = '<table class="models">\n'
    i = 1
    for model in models:
        model_name =  "{}{}".format(name,i)
        (assign, model_html) = print_model(model)
        section += '<tr><td><a name="{m}" class="anchor"></a><span class="modelname">{m}</span></td><td>{a}</td><td>\n'.format(m=model_name,a=assign)
        section += model_html
        section += '</td></tr>\n'
        i += 1
    section += '</table>\n'
    return section

def flatten(l):
    return [item for sublist in l for item in sublist]

def file_is_empty(filename):
    return path.getsize(filename) == 0

def hsv_to_rgb(H,S,V):
    # decompose color range [0,6) into a discrete color (i) and fraction (f)
    h = (H * 6);
    i = int(h);
    f = h - i;
    i = i % 6;

    p = V*(1.0 - S);
    q = V*(1.0 - (S*f));
    t = V*(1.0 - (S*(1.0-f)));

    if i==0:
        return (V,t,p)
    elif i==1:
        return (q,V,p)
    elif i==2:
        return (p,V,t)
    elif i==3:
        return (p,q,V)
    elif i==4:
        return (t,p,V)
    elif i==5:
        return (V,p,q)

def rgb_to_color(rgb):
    (r,g,b) = rgb
    return "{} {} {}".format(r,g,b)

def interpolate(i,total,start,end):
    return start + (end-start)*(i/(total-1))

def get_x3d_of_mds(point_string):
    points = []
    x = []
    y = []
    z = []
    num = dict()

    for line in point_string.strip().split('\n'):
        line.strip()
        point = line.split('\t')
        x.append(float(point[0]))
        y.append(float(point[1]))
        z.append(float(point[2]))
        group = 1
        if len(point) > 3:
            group = int(point[3])
        if group not in num:
            num[group]=0
        num[group] += 1
        points.append((float(point[0]),float(point[1]),float(point[2]),group))

    xmin = min(x)
    xmax = max(x)
    xw = abs(xmax - xmin)

    ymin = min(y)
    ymax = max(y)
    yw = abs(ymax - ymin)

    zmin = min(z)
    zmax = max(z)
    zw = abs(zmax - zmin)

    x3d = ""
    seen = dict()
    for group in num.keys():
        seen[group] = 0

    lines = ""
    spheres = ""
    lastpoint=None
    lastgroup=None
    for (x,y,z,group) in points:
        xx = (x - xmin)/xw*5.0 - 2.5
        yy = (y - ymin)/yw*5.0 - 2.5
        zz = (z - zmin)/zw*5.0 - 2.5

        size = 0.04
        scale = "{s} {s} {s}".format(s=size)

        if group == 1:
            color = rgb_to_color(hsv_to_rgb(interpolate(seen[group], num[group], 11/12, 1),
                                            interpolate(seen[group], num[group], 0.4,      1.0),
                                            1)
                                )
        elif group == 2:
            color = rgb_to_color(hsv_to_rgb(interpolate(seen[group], num[group], 7/12     , 8/12),
                                            interpolate(seen[group], num[group], 0.4,      1.0),
                                            1)
                                )
        elif group == 3:
            color = rgb_to_color(hsv_to_rgb(interpolate(seen[group], num[group], 3/12, 4/12),
                                            interpolate(seen[group], num[group], 0.4,      1.0),
                                            1)
                                )
        elif group == 4:
            color = rgb_to_color(hsv_to_rgb(interpolate(seen[group], num[group], 9/12, 10/12),
                                            interpolate(seen[group], num[group], 0.4,      1.0),
                                            1)
                                )
        else:
            color = "1 0 0"

        sphere = f"<transform translation='{xx} {yy} {zz}' scale='{scale}'><shape><appearance><material specularColor='{color}' shininess='0.8' ambientIntensity='0.8' diffuseColor='{color}'/></material></appearance><sphere></sphere></shape></transform>\n"
        if lastgroup is not None and lastgroup == group:
            tmp=lastpoint+[xx,yy,zz]
            xyzs=' '.join([str(t) for t in tmp])
            line = f"<transform><shape><appearance><material ambientIntensity='0.9' diffuseColor='{color}' specularColor='{color}' transparency='0.95'/></appearance><lineset vertexCount='2'><coordinate point='{xyzs}'/></lineset></shape></transform>\n"
            lines += line
        spheres += sphere
        seen[group] += 1
        lastpoint=[xx,yy,zz]
        lastgroup=group

    x3d = f"{lines}\n{spheres}"
    return x3d

def more_recent_than(f1,f2):
    if not path.exists(f1):
        return False
    return path.getmtime(f1) > path.getmtime(f2)

def more_recent_than_all_of(f1,f2s):
    for f2 in f2s:
        if not more_recent_than(f1,f2):
            return False;
    return True

def find_exe(name,message=None):
    exe = shutil.which(name)
    if exe is None:
        if message is not None:
            print("Program '{}' not found: {}".format(name,message))
        else:
            print("Program '{}' not found.".format(name))
    return exe

def get_n_lines(filename):
    count = 0
    with open(filename) as ifile:
        for line in ifile:
            count += 1
    return count

def get_json_from_file(filename):
    with open(filename, encoding='utf-8') as json_file:
        return json.load(json_file)

def make_extracted(x):
    if not "main" in x:
        return {"main": x, "extracted": []}
    else:
        return x

def all_same(xs):
    if len(xs) < 2:
        return True
    for x in xs:
        if x != xs[0]:
            return False
    return True

def first_all_same(xs):
    if not all_same(xs):
        raise Exception("Not all the same!")
    return xs[0]

def get_unused_dir_name():
    for i in itertools.count(1):
        dirname = "Results.{}".format(i)
        if not path.exists(dirname):
            return dirname

def get_value_from_file(filename,attribute):
    with open(filename,'r',encoding='utf-8') as file:
        for line in file:
            m = re.match('{} ([^ ]*)($| )'.format(attribute), line)
            if m:
                return m.group(1)
    return None

def check_file_exists(filename):
    if not path.exists(filename):
        print("Error: I cannot find file '{}'".format(filename))
        exit(1)
    return filename

def arg_max(xs):
    arg = None
    val = None
    for i in range(len(xs)):
        x = xs[i]
        if val is None or x > val:
            arg = i
            val = x
    return arg

def arg_min(xs):
    arg = None
    val = None
    for i in range(len(xs)):
        x = xs[i]
        if val is None or x < val:
            arg = i
            val = x
    return arg

class MCMCRun(object):
    def __init__(self, mcmc_output,**kwargs):
        self.mcmc_output = mcmc_output
        self.out_file = None
        self.trees_file = None
        self.log_file = None
        self.alignments_files = None
        self.input_files = None
        self.command = None
        self.version = None

        if path.isdir(self.mcmc_output):
            self.dir = self.mcmc_output
            self.prefix = None
        else:
            self.dir = path.dirname(self.mcmc_output)
            self.prefix = path.basename(self.mcmc_output)

    def personality(self):
        return None

    def has_trees(self):
        return self.trees_file

    def has_parameters(self):
        return self.log_file

    def has_alignments(self):
        return self.alignments_files

    def has_model(self):
        return False

    def get_parent_dir(self):
        return path.dirname(path.abspath(self.get_dir()))

    def get_version(self):
        return self.version

    def get_dir(self):
        return self.dir

    def get_prefix(self):
        return self.prefix
    
    def get_out_file(self):
        return self.out_file

    def get_trees_file(self):
        return self.trees_file

    def get_input_files(self):
        return self.input_files

    def get_alignments_files(self):
        return self.alignments_files

    def get_log_file(self):
        return self.log_file

    def get_command(self):
        return self.command

    def input_files(self):
        return self.input_files

    def n_partitions(self):
        return 1

    def get_n_sequences(self,p):
        return None

    def get_alphabets(self):
        return None

    def n_iterations(self):
        return None

    def compute_initial_alignments(self):
        return []

    def get_smodels(self):
        return None

    def get_smodel_indices(self):
        return None

    def get_smodel_for_partition(self,p):
        smodels = self.get_smodels()
        if smodels is None:
            return None

        indices = self.get_smodel_indices()
        if indices is None:
            return None

        index = indices[p]
        if index is None:
            return None

        return smodels[index]

    def get_imodels(self):
        return None

    def get_imodel_for_partition(self,p):
        imodels = self.get_imodels()
        if imodels is None:
            return None

        indices = self.get_imodel_indices()
        if indices is None:
            return None

        index = indices[p]
        if index is None:
            return None

        return imodels[index]

    def get_imodel_indices(self):
        return None

    def get_scale_models(self):
        return None

    def get_scale_model_indices(self):
        return None

    def get_scale_model_for_partition(self,p):
        scale_models = self.get_scale_models()
        if scale_models is None:
            return None

        indices = self.get_scale_model_indices()
        if indices is None:
            return None

        index = indices[p]
        if index is None:
            return None

        return scale_models[index]

    def get_topology_prior(self):
        return None

    def get_branch_length_prior(self):
        return None

class LogFileRun(MCMCRun):
    def __init__(self,mcmc_output,**kwargs):
        super().__init__(mcmc_output, **kwargs)
        self.log_file = mcmc_output
        logging.debug(f"LogFileRun: {mcmc_output}")

    def n_iterations(self):
        n = get_n_lines(self.get_log_file())-1
        if n <= 0:
            print("Error: Log file '{}' has no samples!".format(self.get_log_file()))
            exit(1)
        return n

class TreeFileRun(MCMCRun):
    def __init__(self, mcmc_output, **kwargs):
        super().__init__(mcmc_output, **kwargs)
        self.trees_file = mcmc_output
        logging.debug(f"TreeFileRun: {mcmc_output}")

    def n_iterations(self):
        n = get_n_lines(self.get_trees_file())-1
        if n <= 0:
            print("Error: Tree file '{}' has no samples!".format(self.get_tree_file()))
            exit(1)
        return n

    
class BAliPhyRun(TreeFileRun,LogFileRun):

    def __init__(self,mcmc_output,**kwargs):
        super().__init__(mcmc_output,**kwargs)
        self.prefix = 'C1'
        self.out_file = check_file_exists(path.join(self.get_dir(),'C1.out'))
        self.input_files = self.find_input_file_names_for_outfile(self.out_file)
        self.trees_file = check_file_exists(path.join(self.get_dir(),'C1.trees'))
        self.alignments_files = self.get_alignment_files()
        self.MAP_file = check_file_exists(path.join(self.get_dir(),'C1.MAP'))
        self.command = self.find_header_attribute("command")
        self.version = self.find_header_attribute("VERSION").split('\s+')[0]
        self.find_tree_prior()
        self.smodel_indices = self.find_smodel_indices()
        self.imodel_indices = self.find_imodel_indices()
        self.scale_model_indices = self.find_scale_model_indices()
        self.alphabets = self.find_alphabets()
        self.topology_prior = self.find_in_header('T:topology ')
        self.branch_prior = self.find_in_header('T:lengths ')
        logging.debug(f"BAliPhyRun: {mcmc_output}")

    def get_alphabets(self):
        return self.alphabets

    def has_model(self):
        return True

    def get_smodels(self):
        return self.smodels

    def get_smodel_indices(self):
        return self.smodel_indices

    def get_imodels(self):
        return self.imodels

    def get_imodel_indices(self):
        return self.imodel_indices

    def get_scale_models(self):
        return self.scale_models

    def get_scale_model_indices(self):
        return self.scale_model_indices

    def get_topology_prior(self):
        return self.topology_prior

    def get_branch_length_prior(self):
        return self.branch_prior

    def get_n_sequences(self,p):
        features = self.get_alignment_info(path.join(self.dir,"C1.P{}.initial.fasta".format(p+1)))
        return features["n_sequences"]

    def find_input_file_names_for_outfile(self,outfile):
        input_filenames = []
        with open(outfile,'r',encoding='utf-8') as outf:
            for line in outf:
                m = re.match(r'data(.+) = (.+)', line)
                if m:
                    input_filenames.append(m.group(2))

        return input_filenames

    def get_alignment_files(self):
        filenames = []
        for p in range(1,self.n_partitions()+1):
            filename = path.join(self.get_dir(),'C1.P{}.fastas'.format(p))
            if not path.exists(filename):
                filename = None
            filenames.append(filename)
        return filenames

    def find_header_attribute(self, attribute):
        return self.find_in_header(attribute + ': ')

    def find_in_header(self, key):
        with open(self.out_file,'r',encoding='utf-8') as file:
            reg = key + '(.*)$'
            for line in file:
                m = re.match(reg,line)
                if m:
                    return m.group(1)
        return None

    def n_partitions(self):
        return len(self.get_input_files())

    def find_tree_prior(self):
        with open(self.out_file, encoding='utf-8') as file:
            for line in file:
                m = re.match('^T:topology (.*)$', line)
                if m:
                    self.topology_prior = m.group(1)
                m = re.match('^T:lengths (.*)$', line)
                if m:
                    self.branch_prior = m.group(1)
                if line.startswith("iterations"):
                    break

    def find_partition_values(self,prefix):
        indices = []
        with open(self.out_file,encoding='utf-8') as file:
            regexp = prefix+"(.+) = (.+)"
            for line in file:
                m = re.match(regexp, line)
                if m:
                    indices.append(m.group(2))
                if line.startswith("iterations"):
                    break
        return indices

    def find_smodel_indices(self):
        indices = self.find_partition_values('smodel-index')
        return [None if index == '--' or index == '-1' else int(index) for index in indices]

    def find_imodel_indices(self):
        indices = self.find_partition_values('imodel-index')
        return [None if index == '--' or index == '-1' else int(index) for index in indices]

    def find_scale_model_indices(self):
        indices= self.find_partition_values('scale-index')
        return [None if index == '--' or index == '-1' else int(index) for index in indices]

    def find_alphabets(self):
        return self.find_partition_values('alphabet')

    def compute_initial_alignments(self):
        print("Computing initial alignments: ",end='',flush=True)
        alignment_names = []
        for i in range(self.n_partitions()):
            name="P{}.initial".format(i+1)
            source = path.join(self.dir,"C1."+name+".fasta")
            dest=path.join("Results","Work",name+"-unordered.fasta")
            if not more_recent_than(dest,source):
                shutil.copyfile(source,dest)
            alignment_names.append(name)
        print(" done.")
        return alignment_names

class BAliPhy2_3Run(BAliPhyRun):
    def __init__(self,mcmc_output, **kwargs):
        super().__init__(mcmc_output, **kwargs)
        self.log_file = check_file_exists(path.join(self.get_dir(),'C1.p'))

        self.smodels = self.find_models("subst model", "smodels")
        self.imodels = self.find_models("indel model", "imodels")
        self.scale_models = None
        logging.debug(f"BAliPhy2_3Run: {mcmc_output}")

    def find_models(self, name1, name2):
        models = []
        with open(self.out_file,encoding='utf-8') as file:
            for line in file:
                m = re.match(name1+"([0-9]+) = (.+)", line)
                if m:
                    models.append(m.group(2))
                m = re.match(name1+"([0-9]+) ~ (.+)", line)
                if m:
                    models.append("~"+m.group(2))
                if re.match("^iterations = 0", line):
                    break
        return [make_extracted(model) for model in models]

class BAliPhy3Run(BAliPhyRun):
    def __init__(self,mcmc_output, **kwargs):
        super().__init__(mcmc_output, **kwargs)
        self.log_file = check_file_exists(path.join(self.get_dir(),'C1.log'))
        self.run_file = path.join(self.get_dir(),'C1.run.json')
        if not path.exists(self.run_file):
            self.run_file = None
        self.run_json = get_json_from_file(self.run_file)
        self.version = self.run_json["program"]["version"]
        self.smodels = self.find_models("subst models", "smodels")
        self.imodels = self.find_models("indel models", "imodels")
        self.scale_models = self.find_models("scale model", "scales")
        logging.debug(f"BAliPhy3Run: {mcmc_output}")

    def find_models(self, name1, name2):
        return [make_extracted(model) for model in self.run_json[name2]]

class TreeLogFileRun(TreeFileRun,LogFileRun):
    def __init__(self,mcmc_output, **kwargs):
        super().__init__(mcmc_output, **kwargs)
        logging.debug(f"TreeLogFile Run: {mcmc_output}")

# Since PhyloBayes doesn't make new run files for each dir, maybe we need to file the different prefixes, and pass them to PhyloBayes?
# A prefix could be like dir/prefix (e.g. dir/prefix.trace) or prefix (e.g. prefix.trace)
class PhyloBayesRun(MCMCRun):
    def __init__(self,mcmc_output, **kwargs):
        super().__init__(mcmc_output, **kwargs)
        self.log_file = check_file_exists("{}.trace".format(prefix))
        self.out_file = check_file_exists("{}.log".format(prefix))
        logging.debug(f"PhyloBayesRun: {mcmc_output}")
    
    def personality(self):
        return "phylobayes"

    def get_input_file_names_for_outfile(self,outfile):
        return get_value_from_file(outfile,"data file :")

    def get_n_sequences(self, p):
        return get_value_from_file(self.out_file, "number of taxa:")

class BEASTRun(MCMCRun):
    def __init__(self,mcmc_output, **kwargs):
        super().__init__(mcmc_output, **kwargs)
        for tree_file in glob.glob(path.join(self.get_dir(),'*,trees')):
            check_file_exists(tree_file)
            self.prefix = get_file_prefix(tree_file)
            self.log_file = check_file_exists("{}.log".format(prefix))

def get_bali_phy3_runs(mcmc_args):
    runs = []
    good = []
    bad = []
    for mcmc_dir in mcmc_args:
        if path.exists(mcmc_dir) and path.exists(path.join(mcmc_dir,'C1.out')) and path.exists(path.join(mcmc_dir,'C1.log')):
            runs.append(BAliPhy3Run(mcmc_dir))
            good.append(mcmc_dir)
        else:
            bad.append(mcmc_dir)
    if len(bad) > 0 and len(good) > 0:
        raise Exception(f"{good} appear to be BAli-Phy 3 runs, but {bad} do not!")
    return runs

def get_bali_phy2_3_runs(mcmc_args):
    runs = []
    good = []
    bad = []
    for mcmc_dir in mcmc_args:
        if path.exists(mcmc_dir) and path.exists(path.join(mcmc_dir,'C1.out')) and path.exists(path.join(mcmc_dir,'C1.p')):
            runs.append(BAliPhy2_3Run(mcmc_dir))
            good.append(mcmc_dir)
        else:
            bad.append(mcmc_dir)
    if len(bad) > 0 and len(good) > 0:
        raise Exception(f"{good} appear to be BAli-Phy 2 runs, but {bad} do not!")
    return runs

def get_phylobayes_runs(mcmc_args):
    runs = []
    good = []
    bad = []
    for run_name in mcmc_args:
        if path.exists(run_name+".treelist"):
            runs.append(PhyloBayesRun(run_name))
            good.append(run_name)
        else:
            bad.append(run_name)
    if len(bad) > 0 and len(good) > 0:
        raise Exception(f"{good} appear to be phylobayes runs, but {bad} do not!")
    return runs

def get_tree_log_runs(mcmc_args):
    runs = []
    good = []
    bad = []
    for run_name in mcmc_args:
        if path.exists(run_name+".trees") and path.exists(run_name+".log"):
            runs.append(TreeLogFileRun(run_name))
            good.append(run_name)
        else:
            bad.append(run_name)
    if len(bad) > 0 and len(good) > 0:
        raise Exception(f"{good} appear to be generic tree/parameters runs, but {bad} do not!")
    return runs

def get_tree_runs(mcmc_args):
    runs = []
    good = []
    bad = []
    for run_name in mcmc_args:
        if path.exists(run_name) and run_name.endswith(".trees"):
            runs.append(TreeFileRun(run_name))
            good.append(run_name)
        elif path.exists(run_name+".trees") and not path.exists(run_name+".log"):
            runs.append(TreeFileRun(run_name+".trees"))
            good.append(run_name)
        else:
            bad.append(run_name)
    if len(bad) > 0 and len(good) > 0:
        raise Exception(f"{good} appear to be generic tree-only runs, but {bad} do not!")
    return runs

def get_log_runs(mcmc_args):
    runs = []
    good = []
    bad = []
    for run_name in mcmc_args:
        if path.exists(run_name) and run_name.endswith(".log"):
            runs.append(LogFileRun(run_name))
            good.append(run_name)
        elif not path.exists(run_name+".trees") and path.exists(run_name+".log"):
            runs.append(LogFileRun(run_name+".log"))
            good.append(run_name)
        else:
            bad.append(run_name)
    if len(bad) > 0 and len(good) > 0:
        raise Exception(f"{good} appear to be generic parameter-only runs, but {bad} do not!")
    return runs

def construct_runs(mcmc_outputs):

    for mcmc_output in mcmc_outputs:
        if not glob.glob(mcmc_output+'*'):
            print("No file or directory '{}'".format(mcmc_output+'*'))
            exit(1)

    run_groups = []
    run_groups.append( get_bali_phy3_runs(mcmc_outputs) )

    run_groups.append( get_bali_phy2_3_runs(mcmc_outputs) )

    run_groups.append( get_phylobayes_runs(mcmc_outputs) )

    run_groups.append( get_tree_log_runs(mcmc_outputs) )

    run_groups.append( get_tree_runs(mcmc_outputs) )

    run_groups.append( get_log_runs(mcmc_outputs) )

    for run_group in run_groups:
        if run_group:
            return run_group

    print("Error: MCMC runs '{}' are not BAli-Phy, BEAST, or PhyloBayes run.".format(mcmc_outputs),file=sys.stderr)
    exit(3)
        


class Analysis(object):

    def __init__(self,args,mcmc_outputs):
        self.mcmc_runs = construct_runs(mcmc_outputs)

        self.trees_consensus_exe = find_exe('trees-consensus', message="See the main for adding the bali-phy programs to your PATH.")
        if self.trees_consensus_exe is None:
            exit(1)

        self.draw_tree_exe = find_exe('draw-tree', message="Tree pictures will not be generated.\n")
        # FIXME - maybe switch from gnuplot to R?
        self.gnuplot_exe = find_exe('gnuplot', message='Some graphs will not be generated.\n')
        self.R_exe = find_exe('R', message='Some mixing graphs will not be generated.\n')

        self.subpartitions = args.subpartitions
        self.subsample = args.subsample
        self.prune = args.prune
        self.burnin = args.skip
        self.until = args.until
        self.verbose = args.verbose
        self.speed = 1

        # map from alignment name to alphabet name
        self.alignments = list()

        prefix=path.dirname(path.dirname(self.trees_consensus_exe))
        self.libexecdir = path.join(prefix,"lib","bali-phy","libexec")
        if not path.exists(self.libexecdir):
            print("Can't find bali-phy libexec path '{}'".format(self.libexecdir))

        for i in range(len(self.mcmc_runs)):
            if self.mcmc_runs[i].get_command() != self.mcmc_runs[0].get_command():
                print("WARNING: Commands differ!\n   {}\n   {}\n".format(self.mcmc_runs[0].get_command(),
                                                                         self.mcmc_runs[i].get_command()))
        self.get_input_files()

        self.tree_consensus_levels = [0.5,0.66,0.8,0.9,0.95,0.99,1.0]
        self.alignment_consensus_values = [0.1,0.25,0.5,0.75]

        self.determine_burnin()
        self.initialize_results_directory()

        self.log_shell_cmds = open("Results/commands.log",'a+',encoding='utf-8')
        print("-----------------------------------------------------------",file=self.log_shell_cmds)

        if self.has_parameters():
            self.summarize_numerical_parameters()
        if self.has_trees():
            self.summarize_topology_distribution()
            self.draw_trees()
            self.compute_tree_mixing_diagnostics()
            self.compute_srq_plots()
            self.compute_tree_MDS()
        if self.has_alignments():
            self.compute_initial_alignments()
            self.compute_wpd_alignments()
            self.compute_ancestral_states()
            self.draw_alignments()
            self.compute_and_draw_AU_plots()
        self.print_index_html("Results/index.html")

    def n_chains(self):
        return(len(self.mcmc_runs))

    def has_trees(self):
        return self.mcmc_runs[0].has_trees()

    def has_parameters(self):
        return self.mcmc_runs[0].has_parameters()

    def has_alignments(self):
        return self.mcmc_runs[0].has_alignments()

    def has_model(self):
        return self.mcmc_runs[0].has_model()

    def run(self,p):
        return self.mcmc_runs[p]

    def get_libexec_script(self,file):
        filepath = path.join(self.libexecdir,file)
        if not path.exists(filepath):
            print("Can't find script '{}'!".format(file))
        return filepath

    def Rexec(self,script,args=[]):
        if not self.R_exe:
            return
        return self.exec_show([self.R_exe,'--slave','--vanilla','--args']+args,infile=script)

    def run_gnuplot(self,script):
        if not self.gnuplot_exe:
            return
        subprocess.Popen(self.gnuplot_exe,stdin=subprocess.PIPE).communicate(script.encode('utf-8'))

    def get_input_files(self):
        return first_all_same([run.get_input_files() for run in self.mcmc_runs])

    def n_partitions(self):
        return first_all_same([run.n_partitions() for run in self.mcmc_runs])

    def get_n_sequences(self, p):
        return first_all_same([run.get_n_sequences(p) for run in self.mcmc_runs])

    def get_imodels(self):
        return first_all_same([run.get_imodels() for run in self.mcmc_runs])

    def get_imodel_indices(self):
        return first_all_same([run.get_imodel_indices() for run in self.mcmc_runs])

    def get_imodel_for_partition(self,p):
        return first_all_same([run.get_imodel_for_partition(p) for run in self.mcmc_runs])

    def get_smodels(self):
        return first_all_same([run.get_smodels() for run in self.mcmc_runs])

    def get_smodel_indices(self):
        return first_all_same([run.get_smodel_indices() for run in self.mcmc_runs])

    def get_smodel_for_partition(self,p):
        return first_all_same([run.get_smodel_for_partition(p) for run in self.mcmc_runs])

    def get_scale_models(self):
        return first_all_same([run.get_scale_models() for run in self.mcmc_runs])

    def get_scale_model_indices(self):
        return first_all_same([run.get_scale_model_indices() for run in self.mcmc_runs])

    def get_scale_model_for_partition(self,p):
        return first_all_same([run.get_scale_model_for_partition(p) for run in self.mcmc_runs])

    def get_topology_prior(self):
        return first_all_same([run.get_topology_prior() for run in self.mcmc_runs])

    def get_branch_length_prior(self):
        return first_all_same([run.get_branch_length_prior() for run in self.mcmc_runs])

    def find_models(self, name1, name2):
        return first_all_same([run.find_models(name1,name2) for run in self.mcmc_runs])

    def get_alphabets(self):
        return first_all_same([run.get_alphabets() for run in self.mcmc_runs])

    def get_out_files(self):
        return [run.get_out_file() for run in self.mcmc_runs]

    def get_alignments_files(self):
        return [run.get_alignments_files() for run in self.mcmc_runs]

    def get_log_files(self):
        return [run.get_log_file() for run in self.mcmc_runs]

    def get_trees_files(self):
        return [run.get_trees_file() for run in self.mcmc_runs]

    def get_alignments_for_partition(self,p):
        return [run.get_alignments_files()[p] for run in self.mcmc_runs]

    def exec_show_result(self,cmd,**kwargs):
        showcmd = ' '.join(["'{}'".format(word) for word in cmd])

        subargs = dict()
        if "infile" in kwargs:
            infile = kwargs["infile"]
            subargs["stdin"] = open(infile,encoding='utf-8')
            showcmd += " < '{}'".format(infile)
        elif "stdin" in kwargs:
            subargs["stdin"] = kwargs["stdin"]

        if "outfile" in kwargs:
            outfile = kwargs["outfile"]
            subargs["stdout"] = open(outfile,'w+',encoding='utf-8')
            showcmd += " > '{}'".format(outfile)
        elif "stdout" in kwargs:
            subargs["stdout"] = kwargs["stdout"]
        else:
            subargs["stdout"] = subprocess.PIPE

        if "stderr" in kwargs:
            subargs["stderr"] = kwargs["stderr"]
        else:
            subargs["stderr"] = subprocess.PIPE

        if "cwd" in kwargs:
            subargs["cwd"] = kwargs["cwd"]

        print(showcmd,file=self.log_shell_cmds)
        result = subprocess.run(cmd,**subargs)
        if result.returncode != 0:
            print("command: {}".format(showcmd),file=sys.stderr)
            if "outfile" in kwargs and path.exists(kwargs["outfile"]):
                os.remove(kwargs["outfile"])
            if "handler" in kwargs:
                handler = kwargs["handler"]
                handler(result.returncode)
        elif self.verbose:
            print("\n\t{}\n".format(showcmd))
        return result

    def exec_show(self,cmd,**kwargs):
        result = self.exec_show_result(cmd,**kwargs)

        out_message = None
        if "stdout" not in kwargs and "outfile" not in kwargs:
            out_message = result.stdout.decode('utf-8')

        err_message = None
        if "stderr" not in kwargs:
            err_message = result.stderr.decode('utf-8')

        # Always record error messages in the log file.
        if err_message:
            print("  err: {}".format(err_message),file=self.log_shell_cmds)
        if self.verbose and out_message:
            print("  out: {}".format(out_message),file=self.log_shell_cmds)

        code = result.returncode
        if code != 0:
            print(" exit: {}".format(code),file=sys.stderr)

            print(" exit: {}".format(code),file=self.log_shell_cmds)
            if out_message:
                print("  out: {}".format(out_message),file=self.log_shell_cmds)
                print("  out: {}".format(out_message),file=sys.stdout)
            if err_message:
                print("  err: {}".format(err_message),file=self.log_shell_cmds)

            exit(code)
        return out_message

    def load_analysis_properties(self):
        if not path.exists("Results/properties.json"):
            return dict()
        return get_json_from_file("Results/properties.json")

    def save_analysis_properties(self, properties):
        with open('Results/properties.json', 'w') as outfile:
            json.dump(properties, outfile, indent=2)

    def read_analysis_property(self,key):
        return self.load_analysis_properties()[key]

    def check_analysis_property(self,key,value):
        properties = self.load_analysis_properties()
        return key in properties and value == properties[key]

    def write_analysis_property(self,key,value):
        properties = self.load_analysis_properties()
        properties[key] = value
        self.save_analysis_properties(properties)
        return properties
        
    def write_input_file_names(input_file_names):
        with open("Results/input_files",w,encoding='utf-8') as out:
            print(input_file_names.join('\n'),file=out)

    def initialize_results_directory(self):
        reuse = self.check_analysis_property("burnin", self.burnin)
        reuse = reuse and self.check_analysis_property("subsample", self.subsample)
        reuse = reuse and self.check_analysis_property("until", self.until)
        reuse = reuse and self.check_analysis_property("prune", self.prune)
        reuse = reuse and self.check_analysis_property("alignment_file_names",self.get_alignments_files())
        reuse = reuse and self.check_analysis_property("input_files", self.get_input_files())

        if not reuse and path.exists("Results"):
            new_dir_name = get_unused_dir_name()
            if path.exists("Results"):
                print("Renaming 'Results/' to '{}'\n".format(new_dir_name))
                os.rename("Results",new_dir_name)

        if not path.exists("Results"):
            print("Creating new directory Results/ for summary files.")
            os.mkdir("Results")
            os.mkdir("Results/Work")

        self.write_analysis_property("burnin", self.burnin)
        self.write_analysis_property("subsample", self.subsample)
        self.write_analysis_property("until", self.until)
        self.write_analysis_property("prune", self.prune)
        self.write_analysis_property("input_files", self.get_input_files())
        self.write_analysis_property("alignment_file_names", self.get_alignments_files())

    def determine_burnin(self):
        if self.burnin is not None:
            for run in self.mcmc_runs:
                if self.burnin > run.n_iterations():
                    print("MCMC run {} has only {} iterations.".format(run.mcmc_output, run.n_iterations()))
                    print("Error!  The burnin (specified as {}) cannot be higher than this.".format(self.burnin))
                    exit(1)
            return

        iterations = [run.n_iterations() for run in self.mcmc_runs]

        max_iterations = max(iterations)
        max_i = arg_max(iterations)

        min_iterations = min(iterations)
        min_i = arg_min(iterations)

        if max_iterations > 3*min_iterations:
            print("The variation in run length between MCMC chains is too great.\n")
            print("  Chain #{} has {} iterations.".format(max_i, max_iterations))
            print("  Chain #{} has {} iterations.".format(min_i, min_iterations))
            print("Not going to guess a burnin: please specify one.")
            exit(1)

        self.burnin = int(0.1*min_iterations)

    def summarize_numerical_parameters(self):
        log_files = self.get_log_files()
        if log_files is None or None in log_files:
            return

        print("Summarizing distribution of numerical parameters: ",end='')
        if not more_recent_than_all_of("Results/Report",log_files):
            cmd = ["statreport"] + log_files
            if self.subsample is not None and self.subsample != 1:
                cmd.append("--subsample={}".format(self.subsample))
            if self.burnin is not None:
                cmd.append("--skip={}".format(self.burnin))
            if self.until is not None:
                cmd.append("--until={}".format(self.until))
            self.exec_show(cmd,outfile="Results/Report")
        print("done.")

        print("Analyzing scalar variables: ",end='',flush=True)
        self.median = dict()
        self.CI_low = dict()
        self.CI_high = dict()

        self.ACT = dict()
        self.ESS = dict()
        self.Burnin = dict()
        self.PSRF_CI80 = dict()
        self.PSRF_RCF = dict()

        with open("Results/Report",encoding='utf-8') as report:
            lines = report.readlines()
            i = 0
            while i < len(lines):
                line = lines[i].strip()

                if not line:
                    i += 1
                    continue
                m = re.search('([^\s]+) ~ ([^\s]+)\s+\((.+),(.+)\)',line)
                if m:
                    var = m.group(1)
                    self.median[var] = m.group(2)
                    self.CI_low[var] = m.group(3)
                    self.CI_high[var] = m.group(4)

                    i += 1
                    m = re.search('t @ (.+)\s+Ne = ([^ ]+)\s+burnin = (Not Converged!|[^ ]+)', lines[i])
                    if m:
                        self.ACT[var] = float(m.group(1))
                        self.ESS[var] = float(m.group(2))
                        self.Burnin[var] = m.group(3)
                        if self.Burnin[var] != "Not Converged!":
                            self.Burnin[var] = float(self.Burnin[var])

                    i += 1
                    m = re.search('PSRF-80%CI = ([^ ]+)\s+PSRF-RCF = ([^ ]+)',lines[i])
                    if m:
                        self.PSRF_CI80[var] = float(m.group(1))
                        self.PSRF_RCF[var] = float(m.group(2))
                    i += 1
                    continue

                m = re.search('\s+(.+) = (.+)', line)
                if m:
                    var = m.group(1)
                    self.median[var] = m.group(2)
                i += 1

        self.min_ESS = min(self.ESS.values())
        print("done.")


    def summarize_topology_distribution(self):
        assert(self.has_trees())
        print("\nSummarizing topology distribution: ",end='')
        logging.debug("tree files = {}".format(self.get_trees_files()))
        cmd = ['trees-consensus','--map-tree=Results/MAP.PP.tree','--greedy-consensus=Results/greedy.PP.tree','--report=Results/consensus']+self.get_trees_files()
        cmd.append("--support-levels=Results/c-levels.plot")
        if self.subpartitions:
            cmd.append("--sub-partitions")
            cmd.append("--extended-support-levels=Results/extended-c-levels.plot")
        if self.prune is not None:
            cmd.append("--ignore={}".format(self.prune))
        if self.subsample is not None and self.subsample != 1:
            cmd.append("--subsample={}".format(self.subsample))
        if self.burnin is not None:
            cmd.append("--skip={}".format(self.burnin))
        if self.until is not None:
            cmd.append("--until={}".format(self.until))

        self.trees = [("greedy","greedy"),("MAP","MAP")]
        tree_names=['Results/greedy.PP.tree','Results/MAP.PP.tree']
        consensus_trees=[]
        for level in self.tree_consensus_levels:
            value = int(level*100)
            name = "c{}".format(value)
            filename = "Results/{}.PP.tree".format(name)
            tree_names.append(filename)
            self.trees.append((name,'{}% consensus'.format(value)))
            consensus_trees.append("{}:{}".format(level,filename))
        cmd.append("--consensus={}".format(','.join(consensus_trees)))

        extended_consensus_trees=[]
        for level in self.tree_consensus_levels:
            filename = "Results/c{}.mtree".format(int(level*100))
            extended_consensus_trees.append("{}:{}".format(level,filename))
        if self.subpartitions:
            cmd.append("--extended-consensus={}".format(','.join(extended_consensus_trees)))

        extended_consensus_L=[]
        for level in self.tree_consensus_levels:
            filename = "Results/c{}.mlengths".format(int(level*100))
            extended_consensus_L.append("{}:{}".format(level,filename))
        if self.subpartitions:
            cmd.append("--extended-consensus-L={}".format(','.join(extended_consensus_L)))

        if not more_recent_than_all_of("Results/consensus", self.get_trees_files()):
            self.exec_show(cmd)
        for tree in tree_names:
            if not path.exists(tree) or file_is_empty(tree):
                raise Exception("Tree '{}' not found!".format(tree))
            assert(tree.endswith('.PP.tree'))
            tree2 = tree[0:-8]+'.tree'
            if not more_recent_than(tree2,tree):
                self.exec_show(['tree-tool',tree,'--strip-internal-names','--name-all-nodes'],outfile=tree2)
        print(" done.")

    def get_alignment_info(self,filename):
        output = self.exec_show(['alignment-info',filename])
        features = dict()
        for line in output.split('\n'):
            m = re.match('Alphabet: (.*)$', line)
            if m:
                features["alphabet"] = m.group(1)
                continue

            m = re.match('Alignment: (.+) columns of (.+) sequences', line)
            if m:
                features["length"] = m.group(1)
                features["n_sequences"] = m.group(2)
                continue

            m = re.search('sequence lengths: ([^ ]+)-([^ ]+) ', line)
            if m:
                features["min_length"] = m.group(1)
                features["max_length"] = m.group(2)

            m = re.search(' const.: ([^ ]+) \(([^ ]+)\%\)', line)
            if m:
                features["n_const"] = m.group(1)
                features["p_const"] = m.group(2)

            m = re.search('inform.: ([^ ]+) \(([^ ]+)\%\)', line)
            if m:
                features["n_inform"] = m.group(1)
                features["p_inform"] = m.group(1)

            m = re.search(' ([^ ]+)% minimum sequence identity', line)
            if m:
                features["min_p_identity"] = m.group(1)

        return features

    def draw_trees(self):
        if not self.draw_tree_exe:
            return
        print("Drawing trees: ",end='')
        for level in self.tree_consensus_levels:
            value = int(level*100)
            tree = "c{}".format(value)

            filename1 = "Results/{}.tree".format(tree)
            filename2 = "Results/{}.mtree".format(tree)

            if self.speed < 2 and path.exists(filename2):
                cmd = ['draw-tree', 'Results/{}.mlengths'.format(tree), '--out=Results/{}-mctree'.format(tree), '--draw-clouds=only']
                if not more_recent_than("Results/{}-mctree.svg".format(tree),filename2):
                    self.exec_show(cmd + ['--output=svg'])
                if not more_recent_than("Results/{}-mctree.pdf".format(tree),filename2):
                    self.exec_show(cmd + ['--output=pdf'])

            if not more_recent_than("Results/{}-tree.pdf".format(tree), filename1):
                cmd = ['draw-tree',tree+".tree",'--layout=equal-daylight']
                self.exec_show(cmd,cwd="Results")
            if not more_recent_than("Results/{}-tree.svg".format(tree), filename1):
                cmd = ['draw-tree',tree+".tree",'--layout=equal-daylight','--output=svg']
                self.exec_show(cmd,cwd="Results")
            print(tree+' ',end='',flush=True)

        for tree in ['greedy','MAP']:
            filename = "Results/{}.tree".format(tree)
            if path.exists(filename):
                outname = "Results/{}-tree".format(tree)
                if not more_recent_than(outname+".pdf", filename):
                    self.exec_show(['draw-tree', tree+'.tree','--layout=equal-daylight'],cwd="Results")
                if not more_recent_than(outname+".svg", filename):
                    self.exec_show(['draw-tree', tree+'.tree','--layout=equal-daylight','--output=svg'],cwd="Results")
            print('{} '.format(tree), end='')

        print(". done.")

    def compute_tree_mixing_diagnostics(self):
        print("\nGenerate mixing diagnostics for topologies ...",end='')

        if not more_recent_than("Results/partitions","Results/consensus"):
            self.exec_show(['pickout','--no-header','--large','pi'],
                           infile="Results/consensus",
                           outfile="Results/partitions")

        # This just adds blank lines between the partitions.
        if not more_recent_than("Results/partitions.pred","Results/partitions"):
            with open("Results/partitions",encoding='utf-8') as infile:
                with open("Results/partitions.pred","w+",encoding='utf-8') as outfile:
                    for line in infile:
                        print(line,file=outfile)

        if not more_recent_than_all_of("Results/partitions.bs",self.get_trees_files()):
            cmd = ['trees-bootstrap',
                   '--pred=Results/partitions.pred',
                   '--LOD-table=Results/LOD-table',
                   '--pseudocount=1']
            cmd += self.get_trees_files()
            if self.prune is not None:
                cmd.append("--ignore={}".format(self.prune))
            if self.subsample is not None and self.subsample != 1:
                cmd.append("--subsample={}".format(self.subsample))
            if self.burnin is not None:
                cmd.append("--skip={}".format(self.burnin))
            if self.until is not None:
                cmd.append("--until={}".format(self.until))
            self.exec_show(cmd,outfile="Results/partitions.bs")

        if self.n_chains() < 2:
            print(" done.")
            return

        if not more_recent_than_all_of("Results/convergence-PP.pdf",self.get_trees_files()):
            script = self.get_libexec_script("compare-runs.R")
            self.Rexec(script,["Results/LOD-table","Results/convergence-PP.pdf"])

        if (not more_recent_than_all_of("Results/convergence1-PP.svg",self.get_trees_files()) or
            not more_recent_than_all_of("Results/convergence2-PP.svg",self.get_trees_files())):
            script = self.get_libexec_script("compare-runs2.R")
            self.Rexec(script,["Results/LOD-table","Results/convergence1-PP.svg","Results/convergence2-PP.svg"])
        print(" done.")


    def compute_srq_plots(self):
        self.srq = []
        print("Generate SRQ plot for partitions: ",end='')

        if not more_recent_than_all_of("Results/partitions.SRQ",self.get_trees_files()):
            cmd = ['trees-to-SRQ','Results/partitions.pred','--max-points=1000']
            if self.subsample is not None and self.subsample != 1:
                cmd.append("--subsample={}".format(self.subsample))
            if self.burnin is not None:
                cmd.append("--skip={}".format(self.burnin))
            if self.until is not None:
                cmd.append("--until={}".format(self.until))
            cmd += self.get_trees_files()
            self.exec_show(cmd,outfile="Results/partitions.SRQ")
        print("done.");
        self.srq.append("partitions")

        print("Generate SRQ plot for c50 tree: ", end='')
        if not more_recent_than_all_of("Results/c50.SRQ", self.get_trees_files()):
            cmd = ['trees-to-SRQ','Results/c50.tree','--max-points=1000']
            if self.subsample is not None and self.subsample != 1:
                cmd.append("--subsample={}".format(self.subsample))
            if self.burnin is not None:
                cmd.append("--skip={}".format(self.burnin))
            if self.until is not None:
                cmd.append("--until={}".format(self.until))
            cmd += self.get_trees_files()
            self.exec_show(cmd,outfile="Results/c50.SRQ")
        print("done.");
        self.srq.append("c50")

        for srq in self.srq:
            cmd = ['gnuplot']
            self.run_gnuplot("""\
set terminal png size 300,300
set output "Results/{srq}.SRQ.png"
set key right bottom
set xlabel "Regenerations (fraction)"
set ylabel "Time (fraction)"
set title "Scaled Regeneration Quantile (SRQ) plot: $srq"
plot "Results/{srq}.SRQ" title "{srq}" with linespoints lw 1 lt 1, x title "Goal" lw 1 lt 3
""".format(srq=srq))
        if self.subpartitions:
            self.run_gnuplot("""\
set terminal svg
set output "Results/c-levels.svg"
set xlabel "Log10 posterior Odds (LOD)"
set ylabel "Supported Splits"
set style data lines
plot [0:][0:] 'Results/c-levels.plot' title 'Full Splits','Results/extended-c-levels.plot' title 'Partial Splits'""")
        else:
            self.run_gnuplot("""\
set terminal svg
set output "Results/c-levels.svg"
set xlabel "Log10 posterior Odds (LOD)"
set ylabel "Supported Splits"
plot [0:][0:] 'Results/c-levels.plot' with lines notitle
""")
    def compute_tree_MDS(self):
        if not self.R_exe:
            return
        print ("\nGenerate MDS plots of topology burnin: ", end='',flush=True)

        C = min(self.n_chains(),4)
        N = int(800/self.n_chains())
        dist_cmd = ['trees-distances','matrix',f'--max={N}', '--jitter=0.3']

        if self.subsample is not None and self.subsample != 1:
            dist_cmd.append("--subsample={}".format(self.subsample))

        if self.burnin is not None:
            dist_cmd.append("--skip={}".format(self.burnin))

        if self.until is not None:
            dist_cmd.append("--until={}".format(self.until))

        tree_files = self.get_trees_files()[0:C]
        matfile = "Results/tree-MDS.M"

        script = self.get_libexec_script(f"tree-plot.R")
        outfile = "Results/tree-MDS.svg"

        script3d = self.get_libexec_script(f"tree-plot-3D.R")
        outfile3d = "Results/tree-MDS.points.html"

        L = [ str(min([N,int((get_n_lines(tf)-self.burnin)/self.subsample)])) for tf in tree_files]

        if not more_recent_than_all_of(matfile, tree_files):
            self.exec_show(dist_cmd+tree_files, outfile=matfile)

        if not more_recent_than(outfile, matfile):
            self.Rexec(script,[matfile,outfile]+L)

        if not more_recent_than(outfile3d,matfile):
            point_string = self.Rexec(script3d, [matfile]+L)
            self.write_x3d_file("Results", "tree-MDS.points", point_string)

        print(" done.")

    def write_x3d_file(self,dir,filename,point_string):
        assert(point_string is not None)

        if dir:
            filename = path.join(dir,filename+".html")
        with open(filename,"w+",encoding='utf-8') as x3d_file:
            print("""\
<html>
 <head>
   <title>MDS 3D Plot</title>
   <script type='text/javascript' src='http://www.x3dom.org/download/x3dom.js'> </script>
   <link rel='stylesheet' type='text/css' href='http://www.x3dom.org/download/x3dom.css'></link>
    <style>
      x3d { border:2px solid darkorange; }
    </style>
  </head>
  <body>
    <x3d width='1000px' height='1000px'>
      <scene>""",file=x3d_file)
            print(get_x3d_of_mds(point_string),file=x3d_file)
            print("""\
      </scene>
    </x3d>
  </body>
</html>""",file=x3d_file)

    def compute_initial_alignments(self):
        names = self.run(0).compute_initial_alignments()
        for i in range(len(names)):
            name = names[i]
            self.alignments.append((name, self.get_alphabets()[i], "Initial"))
            self.make_ordered_alignment(name)


    def compute_wpd_alignments(self):
        print("\nComputing WPD alignments: ", end='',flush=True)
        for i in range(self.n_partitions()):
            if self.get_imodel_for_partition(i) is None:
                continue
            afiles = self.get_alignments_for_partition(i)
            name = "P{}.max".format(i+1)
            self.alignments.append((name, self.get_alphabets()[i], "Best (WPD)"))
            if not more_recent_than_all_of("Results/Work/{}-unordered.fasta".format(name),afiles):
                cut_cmd=['cut-range']+afiles
                if self.burnin is not None:
                    cut_cmd.append("--skip={}".format(self.burnin))
                if self.until is not None:
                    cut_cmd.append("--until={}".format(self.until))
                p1 = subprocess.Popen(cut_cmd,  stdout=subprocess.PIPE)

                chop_cmd=['alignment-chop-internal','--tree=Results/MAP.tree']
                p2 = subprocess.Popen(chop_cmd, stdout=subprocess.PIPE, stdin=p1.stdout)

                max_cmd=['alignment-max']
                self.exec_show(max_cmd,stdin=p2.stdout, outfile="Results/Work/{}-unordered.fasta".format(name))

                p1.wait()
                p2.wait()
                self.make_ordered_alignment(name)
        print(" done.")

    def reorder_alignment_by_tree(self,alignment,tree,outfile):
        if not path.exists(tree):
            print("Can't reorder alignment by tree '{}': tree file does not exist!".format(tree))
        if file_is_empty(tree):
            print("Can't reorder alignment by tree '{}': tree file is empty!".format(tree))
        if not path.exists(alignment):
            print("Can't reorder alignment '{}' by tree: alignment file does not exist!".format(tree))
        if file_is_empty(alignment):
            print("Can't reorder alignment '{}' by tree: alignment file is empty!".format(tree))
        if not more_recent_than_all_of(outfile,[tree,alignment]):
            cmd = ['alignment-cat', alignment, '--reorder-by-tree={}'.format(tree)]
            self.exec_show(cmd, outfile=outfile)
        assert(path.exists(outfile))

    def make_ordered_alignment(self,alignment):
        ufilename = "Results/Work/{}-unordered.fasta".format(alignment)
        filename = "Results/{}.fasta".format(alignment)
        if not more_recent_than_all_of(filename, [ufilename,"Results/c50.tree"] ):
            self.reorder_alignment_by_tree(ufilename,"Results/c50.tree",filename)
        assert(path.exists(filename))
        return filename

    def color_scheme_for_alphabet(self,alphabet):
        if alphabet == "Amino-Acids":
            return "AA+contrast"
        else:
            return "DNA+contrast"

    def draw_alignment(self,filename,**kwargs):
        cmd = ['alignment-draw',filename]

        if "color_scheme" in kwargs:
            color_scheme = kwargs["color_scheme"]
            if color_scheme is not None:
                cmd += ['--color-scheme',color_scheme]

        if "ruler" in kwargs:
            if kwargs["ruler"]:
                cmd += ['--show-ruler']

        if "AU" in kwargs:
            AU = kwargs["AU"]
            if AU is not None:
                cmd += ['--AU',AU]

        if "outfile" in kwargs:
            outfile = kwargs["outfile"]
        else:
            outfile = None
        if outfile is None:
            outfile = os.path.splitext(filename)[0]+'.html'

        self.exec_show(cmd,outfile=outfile)
        return outfile

    def draw_alignments(self):
        if not self.alignments:
            return

        print("Drawing alignments: ", end='', flush=True)
        for (alignment,alphabet,name) in self.alignments:
            filename = "Results/{}.fasta".format(alignment)
            color_scheme = self.color_scheme_for_alphabet(alphabet)
            self.draw_alignment(filename, color_scheme=color_scheme, ruler=True)
            print('*',end='',flush=True)

        for i in range(self.n_partitions()):
            if self.get_imodel_for_partition(i) is None:
                continue

            outfile = "Results/P{}.initial-diff.AU".format(i+1)
            initial = "Results/P{}.initial.fasta".format(i+1)
            wpd = "Results/P{}.max.fasta".format(i+1)
            self.exec_show(['alignments-diff',initial,wpd],outfile=outfile)

            outhtml = "Results/P{}.initial-diff.html".format(i+1)
            self.exec_show(['alignment-draw',initial,'--scale=identity','--AU',outfile,'--show-ruler','--color-scheme=diff[1]+contrast'],outfile=outhtml)
            print("*",end='',flush=True);

        print(" done.")


    def compute_ancestral_states(self):
        print("Computing ancestral state alignment: ",end='',flush=True)
        for i in range(self.n_partitions()):
            afiles = self.get_alignments_for_partition(i)
            name = 'P{}.ancestors'.format(i+1)
            if self.get_imodel_for_partition(i) is None:
                bad = 0
                for afile in afiles:
                    if afile is None or not path.exists(afile):
                        bad += 1
                if bad > 0:
                    continue
            assert(len(afiles) == self.n_chains())

            self.alignments.append((name,self.get_alphabets()[i], "Ancestral"))

            template = "Results/P{}.max.fasta".format(i+1)
            if self.get_imodel_for_partition(i) is None:
                template = "Results/P{}.initial.fasta".format(i+1)
            tree = "Results/c50.tree"
            cmd = ['summarize-ancestors',template,'-n',tree,'-g',tree]
            for dir in [run.dir for run in self.mcmc_runs]:
                cmd += ['-A',path.join(dir,'C1.P{}.fastas'.format(i+1)),
                        '-T',path.join(dir,'C1.trees')]
            output = "Results/P{}.ancestors.fasta".format(i+1)
            if not more_recent_than_all_of(output,self.get_trees_files()+list(filter(lambda x:x is not None,flatten(self.get_alignments_files())))):
                self.exec_show(cmd,outfile=output)
        print(" done.")

    def compute_and_draw_AU_plots(self):
        for (alignment,alphabet,name) in self.alignments:
            # Don't try and compute AU plots for ancestral sequences
            m = re.match('^P([0-9]+).ancest.*',alignment)
            if m:
                continue

            m = re.match('^P([0-9]+).*',alignment)
            if m:
                i = int(m.group(1))-1
                afile = "Results/{}.fasta".format(alignment)
                afiles = self.get_alignments_for_partition(i)
                if None in afiles:
                    continue
                print("Generating AU values for '{}'...".format(alignment),end='',flush=True)
                AUfile = "Results/{}-AU.prob".format(alignment)
                if not more_recent_than_all_of(AUfile, afiles):
                    cut_cmd=['cut-range']+afiles
                    if self.burnin is not None:
                        cut_cmd.append("--skip={}".format(self.burnin))
                    if self.until is not None:
                        cut_cmd.append("--until={}".format(self.until))
                    p1 = subprocess.Popen(cut_cmd,  stdout=subprocess.PIPE)

                    chop_cmd=['alignment-chop-internal','--tree=Results/MAP.tree']
                    p2 = subprocess.Popen(chop_cmd, stdout=subprocess.PIPE, stdin=p1.stdout)

                    gild_cmd = ['alignment-gild',afile,'Results/MAP.tree','--max-alignments=500']
                    self.exec_show(gild_cmd, stdin=p2.stdout, outfile=AUfile)

                    p1.wait()
                    p2.wait()

                html_file = "Results/{}-AU.html".format(alignment)
                if not more_recent_than_all_of(html_file,[afile,AUfile]):
                    color_scheme=self.color_scheme_for_alphabet(alphabet)
                    color_scheme += "+fade+fade+fade+fade"
                    self.draw_alignment(afile,ruler=True,AU=AUfile,color_scheme=color_scheme,outfile=html_file)
                print(' done.')



    def print_index_html(self,filename):
        with open(filename,"w+",encoding='utf-8') as index:
            title = "MCMC Post-hoc Analysis"
            output  = self.html_header(title)

            #FIXME: We should only write sections that are not empty for this analysis!
            output += self.topbar()
            output += '<div class="content">\n'
            output += "<h1>{}</h1>\n".format(title)
            if self.has_model():
                output += self.section_data_and_model()
            if self.has_parameters():
                output += self.section_scalar_variables()
            if self.has_trees():
                output += self.section_phylogeny_distribution()
            if self.has_alignments():
                output += self.section_alignment_distribution()
            output += self.section_ancestral_sequences() # FIXME!
            output += self.section_mixing()
            output += self.section_analysis()
            if self.has_model():
                output += self.section_model_and_priors()
            output += self.section_end()
            print(output, file=index)

        print("\nReport written to '{}".format(filename));

    def html_header(self,title):
        section = """\
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
"""
        section += "<title>BAli-Phy: {}</title>\n".format(title);
        section += """\
      <style type="text/css">
      body {margin:0; padding:0}
      ol li {padding-bottom:0.5em}

      th {padding-left: 0.5em; padding-right:0.5em}
      td {padding: 0.1em;}
      .backlit2 td {padding-left: 0.5em;}
      .backlit2 td {padding-right: 1.0em;}

#topbar {
	background-color: rgb(201,217,233);
	margin: 0;
	padding: 0.5em;
	display: table;
        width: 100%;
        position: fixed;
        top: 0;
}

#topbar #menu {
//	font-size: 90%;
	display: table-cell;
	text-align: right;
        // Leave space for the menubar
        padding-right: 0.75em;
}
#topbar #path {
	font-weight: bold;
	display: table-cell;
	text-align: left;
	white-space: nowrap;
}

      .content {margin:1em; margin-top: 3em}
      .backlit td {background: rgb(220,220,220);}

      :target:before {
        content:"";
        display:block;
        height:2em; /* fixed header height*/
        margin:-2em 0 0; /* negative fixed header height */
      }

      h1 {font-size: 150%;}
      h2 {font-size: 130%; margin-top:2.5em; margin-bottom: 1em}
      h3 {font-size: 110%; margin-bottom: 0.2em}// margin-top: 0.3em}

      ul {margin-top: 0.4em;}

      .center td {text-align: center};

      *[title] {cursor:help;}
      a[title] {vertical-align:top;color:#00f;font-size:70%; border-bottom:1px dotted;}
      .floating_picture {float:left;padding-left:0.5em;padding-right:0.5em;margin:0.5em;}
      .r_floating_picture {float:right;padding-left:0.5em;padding-right:0.5em;margin:0.5em;}
      table.backlit2 {border-collapse: collapse; }
      table.backlit2 tr:nth-child(odd) td { background-color: #F0F0F0; margin:0}
      .clear {clear:both;}

      .modelname {font-weight: bold; color: blue}
      table.model {margin-left: 2em}
      table.model td {vertical-align: top}
      table.models td {vertical-align: top}

    </style>
</head>
<body>
"""
        return section

    def topbar(self):
        return """\
  <p id="topbar">
    <span id="path">Sections:</span>
    <span id="menu">
      [<a href="#data">Data+Model</a>]
      [<a href="#parameters">Parameters</a>]
      [<a href="#topology">Phylogeny</a>]
      [<a href="#alignment">Alignment</a>]
      [<a href="#mixing">Mixing</a>]
      [<a href="#analysis">Analysis</a>]
      [<a href="#models">Models+Priors</a>]
    </span>
"""

    def section_data_and_model(self):
        section = """\
<h2 style="clear:both"><a class="anchor" name="data"></a>Data &amp; Model</h2>
<table class="backlit2">
  <tr><th>Partition</th><th>Sequences</th><th>Lengths</th><th>Alphabet</th><th>Substitution&nbsp;Model</th><th>Indel&nbsp;Model</th><th>Scale&nbsp;Model</th></tr>
"""
        for i in range(self.n_partitions()):
            section += '<tr>\n'
            section += '  <td>{}</td>\n'.format(i+1)
            section += '  <td>{}</td>\n'.format(self.get_input_files()[i])

            features = self.get_alignment_info("Results/P{}.initial.fasta".format(i+1))
            minlength = features['min_length']
            maxlength = features['max_length']
            section += '<td>{} - {}</td>\n'.format(minlength,maxlength)

            alphabet = self.get_alphabets()[i]
            section += '<td>{}</td>\n'.format(alphabet)

            smodel = self.get_smodel_for_partition(i)
            if smodel:
                smodel = smodel['main']
                index = self.get_smodel_indices()[i]+1
                target = "S{}".format(index)
                link = '<a href="#{t}">{t}</a>'.format(t=target)
                section += '<td>{} = {}</td>\n'.format(link,smodel)
            elif self.get_smodels() is not None:
                section += '<td>none</td>\n'
            else:
                section += '<td></td>\n'

            imodel = self.get_imodel_for_partition(i)
            if imodel:
                imodel = imodel['main']
                index = self.get_imodel_indices()[i]+1
                target = "I{}".format(index)
                link = '<a href="#{t}">{t}</a>'.format(t=target)
                section += '<td>{} = {}</td>\n'.format(link,imodel)
            elif self.get_imodels() is not None:
                section += '<td>none</td>\n'
            else:
                section += '<td></td>\n'

            scale_model = self.get_scale_model_for_partition(i)
            if scale_model:
                scale_model = scale_model['main']
                index = self.get_scale_model_indices()[i]+1
                target = "scale{}".format(index)
                link = '<a href="#{t}">{t}</a>'.format(t=target)
                assign = '='
                if scale_model[0] == '~':
                    scale_model = scale_model[1:]
                    assign = '~'
                section += '<td>{} {} {}</td>\n'.format(link,assign,scale_model)
            elif self.get_scale_models() is not None:
                section += '<td>none</td>\n'
            else:
                section += '<td></td>\n'

            section += '</tr>\n'
        section += '</table>\n'
        return section

    def section_scalar_variables(self):
        log_files = self.get_log_files()
        if log_files is None or None in log_files:
            return

        section = """\
<h2 class="clear"><a class="anchor" name="parameters"></a>Scalar variables</h2>
    <table class="backlit2">
        <tr><th>Statistic</th><th>Median</th><th title="95% Bayesian Credible Interval">95% BCI</th><th title="Auto-Correlation Time">ACT</th><th title="Effective Sample Size">ESS</th><th>burnin</th><th title="Potential Scale Reduction Factor based on width of 80% credible interval">PSRF-CI80%</th><th>PSRF-RCF</th></tr>
"""
        for var in self.median:
            if var == "iter":
                continue
            if var == "time" and self.personality() == "phylobayes":
                continue
            if var == "#treegen" and self.personality() == "phylobayes":
                continue

            row_style = "" if var in self.CI_low else 'style="font-style:italic"'
            section += '<tr {}>\n'.format(row_style)
            section += '  <td>{}</td>\n'.format(var)
            section += '  <td>{}</td>\n'.format(self.median[var])
            if var in self.CI_low:
                section += '  <td>({}, {})</td>\n'.format(self.CI_low[var], self.CI_high[var])

                ACT_style = ' style="color:red"' if self.ESS[var] <= self.min_ESS else ""
                section += '  <td {}>{}</td>\n'.format(ACT_style,self.ACT[var])

                if self.ESS[var] < 100:
                    ESS_style = ' style="color:red"'
                elif self.ESS[var] < 300:
                    ESS_style = ' style="color:orange"'
                else:
                    ESS_style = ''
                section += '  <td {}>{}</td>\n'.format(ESS_style,self.ESS[var])

                burnin_style = ' style="color:red"' if self.Burnin[var] == "Not Converged!" else ""
                section += '  <td {}>{}</td>\n'.format(burnin_style,self.Burnin[var])

                if var in self.PSRF_CI80:
                    if self.PSRF_CI80[var] >= 1.2:
                        CI80_style = ' style="color:red"'
                    elif self.PSRF_CI80[var] >= 1.05:
                        CI80_style = ' style="color:orange"'
                    else:
                        CI80_style = ''
                    section += '  <td {}>{}</td>\n'.format(CI80_style,self.PSRF_CI80[var])
                else:
                    section += '  <td>NA</td>\n'

                if var in self.PSRF_RCF:
                    if self.PSRF_RCF[var] >= 1.2:
                        RCF_style = ' style="color:red"'
                    elif self.PSRF_RCF[var] >= 1.05:
                        RCF_style = ' style="color:orange"'
                    else:
                        RCF_style = ''
                    section += '  <td {}>{}</td>\n'.format(RCF_style,self.PSRF_RCF[var])
                else:
                    section += '  <td>NA</td>\n'

            else:
                section += '  <td></td>\n'
                section += '  <td></td>\n'
                section += '  <td></td>\n'
                section += '  <td></td>\n'
                section += '  <td></td>\n'
                section += '  <td></td>\n'

            section += '</tr>\n'
        section += '</table>\n'

        return section

    def section_phylogeny_distribution(self):
        section = '<h2><a class="anchor" name="topology"></a>Phylogeny Distribution</h2>\n'
        section += self.html_svg('c-levels.svg','35%','',['r_floating_picture'])
        section += self.html_svg('c50-tree.svg','25%','',['floating_picture'])

        section += """\
<table>
  <tr>
    <td>Partition support: <a href="consensus">Summary</a></td>
    <td><a href="partitions.bs">Across chains</a></td>
  </tr>
</table>
"""
        section += '<table>\n'
        for (tree,name) in self.trees:
            section += """\
  <tr>
    <td>{name}</td>
    <td><a href="{tree}.tree">Newick</a></td>
    <td>(<a href="{tree}.PP.tree">+PP</a>)</td>
    <td><a href="{tree}-tree.pdf">PDF</a></td>
    <td><a href="{tree}-tree.svg">SVG</a></td>
""".format(name=name,tree=tree)
            if self.subpartitions and (path.exists("Results/{}.mtree".format(tree)) or
                                       path.exists("Results/{}-mctree.svg".format(tree)) or
                                       path.exists("Results/{}-mctree.pdf".format(tree))):
                section += '<td>MC Tree:</td>\n'
            else:
                section +='<td></td>'

            if self.subpartitions and path.exists("Results/{}.mtree".format(tree)):
                section += '<td><a href="{}.mtree">-L</a></td>\n'.format(tree)
            else:
                section +='<td></td>'

            if self.subpartitions and path.exists("Results/{}-mctree.pdf".format(tree)):
                section += '<td><a href="{}-mctree.pdf">PDF</a></td>\n'.format(tree)
            else:
                section +='<td></td>'

            if self.subpartitions and path.exists("Results/{}-mctree.svg".format(tree)):
                section += '<td><a href="{}-mctree.svg">SVG</a></td>\n'.format(tree)
            else:
                section +='<td></td>'

            section += '  </tr>\n'
        section += '</table>'
        return section

    def section_alignment_distribution(self):
        if not self.n_partitions():
            return

        section = '<h2 class="clear"><a class="anchor" name="alignment"></a>Alignment Distribution</h2>\n'
        for i in range(self.n_partitions()):
            section += '<h3>Partition {}</h3>\n'.format(i+1)
            section += """\
  <table>
    <tr>
       <th style="padding-right:3em"></th>
       <th></th>
       <th></th>
       <th title ="Comparison of this alignment (top) to the WPD alignment (bottom)">Diff</th>
       <th></th>
       <th style="padding-right:0.5em;padding-left:0.5em" title="Percent identity of the most dissimilar sequences">Min. %identity</th>
       <th style="padding-right:0.5em;padding-left:0.5em" title="Number of columns in the alignment"># Sites</th>
       <th style="padding-right:0.5em;padding-left:0.5em" title="Number of invariant columns">Constant</th>
       <th title="Number of parsimony-informative columns.">Informative</th>
    </tr>
""".format(i+1)
            for (alignment,alphabet,name) in self.alignments:
                if not alignment.startswith('P{}.'.format(i+1)):
                    continue
                section += '    <tr>\n'
                section += '      <td style="padding-right:3em">{}</td>\n'.format(name)
                section += '      <td><a href="{}.fasta">FASTA</a></td>\n'.format(alignment)

                if path.exists(path.join("Results",alignment+".html")):
                    section += '      <td><a href="{}.html">HTML</a></td>\n'.format(alignment)
                else:
                    section += '      <td></td>\n'

                if path.exists(path.join("Results",alignment+"-diff.html")):
                    section += '      <td><a href="{}-diff.html">Diff</a></td>\n'.format(alignment)
                else:
                    section += '      <td></td>\n'

                if path.exists(path.join("Results",alignment+"-AU.html")):
                    section += '      <td><a href="{}-AU.html">AU</a></td>\n'.format(alignment)
                else:
                    section += '      <td></td>\n'

                features = self.get_alignment_info("Results/{}.fasta".format(alignment))
                section += '<td style="text-align: center">{}%</td>\n'.format(features['min_p_identity'])
                section += '<td style="text-align: center">{}</td>\n'.format(features['length'])
                section += '<td style="text-align: center">{} ({}%)</td>\n'.format(features['n_const'], features['p_const'])
                section += '<td style="text-align: center">{} ({}%)</td>\n'.format(features['n_inform'], features['p_inform'])

                section += '    </tr>\n'
            section += '  </table>\n'
        return section

    def get_value_from_file(self,filename,attribute):
        with open(filename,encoding='utf-8') as file:
            for line in file:
                m = re.search(attribute + ' ([^ ]*)($| )', line)
                if m:
                    return m.group(1)
        return None

    def html_svg(self,url,width=None,height=None,classes=[],extra=None):
        classes.append('svg-image')
        class_=' '.join(classes)
        svg = '<object data="{}" type="image/svg+xml" class="{}"'.format(url,class_)
        if width:
            svg += " width={}".format(width)
        if height:
            svg += " height={}".format(height)
        if extra:
            svg += " "+extra
        svg += ' ></object>'
        return svg

    def section_ancestral_sequences(self):   #REMOVE!
        section = ""
        return section

    def section_tree_mixing2(self):
        section = ""
        assert(self.has_trees())
        MDS_figure = "tree-MDS.svg"
        MDS_figure_3d = "tree-MDS.points"
        C = min(self.n_chains(),4)
        if self.n_chains() > 4:
            MDS_title = "the first 4 chains"
        else:
            MDS_title = f"{C} chains"

        section += """\
<table style="width:100%;clear:both">
  <tr>
    <td style="width:40%;vertical-align:top">
      <h4 style=\"text-align:center\">Projection of RF distances for {} (<a href=\"https://doi.org/10.1080/10635150590946961\">Hillis et al 2005</a>)</h4>
""".format(MDS_title)
        if path.exists(path.join("Results",MDS_figure)):
            section += self.html_svg(MDS_figure,"90%","",[])
            section += '<a href="{}.html">3D</a>'.format(MDS_figure_3d)
        else:
            if not self.R_exe:
                section += "<p>Not generated: can't find R</p>"
        section += '</td>'
        section += '<td style="width:40%;vertical-align:top">'
        section += '<h4 style="text-align:center">Variation of split PPs across chains (<a href="https://doi.org/10.1080/10635150600812544">Beiko et al 2006</a>)</h4>'
        if path.exists("Results/convergence1-PP.svg"):
            section += self.html_svg("convergence1-PP.svg","90%","",[])
        else:
            if not self.R_exe:
                section += "<p>Not generated: can't find R.</p>"
            elif self.n_chains() == 1:
                section += "<p>Not generated: multiple chains needed.</p>"
            else:
                section += "<p>Not generated.</p>"

        if path.exists("Results/convergence2-PP.svg"):
            section += "<br/><br/><br/><br/>"
            section += self.html_svg("convergence2-PP.svg","90%","",[])
        section += "</td>"
        section += "</tr></table>"
        return section

    def section_mixing(self):
        section = """\
<h2><a class="anchor" name="mixing"></a>Mixing</h2>
  <table style="width:100%;clear:both">
    <tr>
      <td style="vertical-align:top">
        <p><b>Statistics:</b></p>
        <table class="backlit2">
"""
        burnin_before = "NA"
        min_NE = "NA"
        if self.has_parameters():
            if self.get_log_files():
                burnin_before = self.get_value_from_file("Results/Report", 'min burnin <=')
                if burnin_before == "Not":
                    burnin_before = "Not Converged!"
                min_NE = self.get_value_from_file("Results/Report", 'Ne  >=')
            section += "<tr><td><b>scalar burnin</b></td><td>{}</td></tr>\n".format(burnin_before)
            section += "<tr><td><b>scalar ESS</b></td><td>{}</td></tr>\n".format(min_NE)


        if self.has_trees():
            min_NE_partition = self.get_value_from_file('Results/partitions.bs','min Ne =')
            section += '<tr><td><b title=\"Effective Sample Size for bit-vectors of partition support (smallest)\">topological ESS</b></td><td>{}</td></tr>\n'.format(min_NE_partition)

            asdsf = self.get_value_from_file('Results/partitions.bs','ASDSF\[min=0.100\] =')
            if asdsf is None:
                asdsf = "NA"
            section += '<tr><td><b title=\"Average Standard Deviation of Split Frequencies\">ASDSF</b></td><td>{}</td></tr>'.format(asdsf)

            msdsf = self.get_value_from_file('Results/partitions.bs','MSDSF =')
            if msdsf is None:
                msdsf = "NA"
            section += '<tr><td><b title=\"Maximum Standard Deviation of Split Frequencies\">MSDSF</b></td><td>{}</td></tr>'.format(msdsf)

        psrf_80 = 'NA'
        psrf_rcf = 'NA'
        if self.has_parameters():
            if self.get_log_files() and len(self.get_log_files()) >= 2:
                psrf_80 = self.get_value_from_file('Results/Report','PSRF-80%CI <=')
                psrf_rcf = self.get_value_from_file('Results/Report','PSRF-RCF <=')
            section += '<tr><td><b>PSRF CI80%</b></td><td>{}</td><tr>\n'.format(psrf_80)
            section += '<tr><td><b>PSRF RCF</b></td><td>{}</td><tr>\n'.format(psrf_rcf)

        section += '</table>\n'
        section += '</td>\n'

        section += '    <td>\n'
        if self.has_trees():
            section += '      <img src="c50.SRQ.png" alt="SRQ plot for supprt of 50% consensus tree."/>'
            section += '      <img src="partitions.SRQ.png" alt="SRQ plot for supprt of each partition."/>'
        section += '    </td>\n'
        section += '  </tr></table>\n'

        if self.has_trees():
            section += self.section_tree_mixing2()

        print("")
        if burnin_before:
            print("NOTE: burnin (scalar) <= {}".format(burnin_before))
        if min_NE:
            print("NOTE: min_ESS (scalar)    = {}".format(min_NE))
        if self.has_trees():
            if min_NE_partition:
                print("NOTE: min_ESS (partition)    = {}".format(min_NE_partition))
            if asdsf:
                print("NOTE: ASDSF = {}".format(asdsf))
            if msdsf:
                print("NOTE: MSDSF = {}".format(msdsf))
        if psrf_80:
            print("NOTE: PSRF-80%CI = {}".format(psrf_80))
        if psrf_rcf:
            print("NOTE: PSRF-RCF = {}".format(psrf_rcf))
        return section

    def section_analysis(self):
        section = ""

        commands = [run.get_command() for run in self.mcmc_runs]
        versions = [run.get_version() for run in self.mcmc_runs]
        parent_dirs = [run.get_parent_dir() for run in self.mcmc_runs]

        something_common = all_same(commands) or all_same(versions) or all_same(parent_dirs)

        section += '<br/><hr/><br/>\n'
        section += '<h2><a class="anchor" name="analysis"></a>Analysis</h2>\n'
        if something_common:
            section += "<p>"
        if all_same(commands):
            section += "<b>command line</b>: {}</br>\n".format(commands[0])
        if all_same(parent_dirs):
            section += "<b>directory</b>: {}</br>\n".format(parent_dirs[0])
        if all_same(versions):
            section += "<b>version</b>: {}\n".format(versions[0])
        if something_common:
            section += "</p>"
        #section += '<table style="width:100%">'."\n"

        #section += '<table class="backlit2 center" style="width:100%">'."\n"
        section += '<table class="backlit2 center">\n'
        section += "<tr><th>chain #</th>"
        if not all_same(versions):
            section += "<th>version</th>" 
        section += "<th>burnin</th><th>subsample</th><th>samples</th>"
        if not all_same(commands):
            section += "<th>command line</th>"
        section += "<th>subdirectory</th>"
        if not all_same(parent_dirs):
            section += "<th>directory</th>"
        section += "</tr>\n"

        for i in range(self.n_chains()):
            section += "<tr>\n"

            section += "  <td>{}</td>\n".format(i+1)
            if not all_same(versions):
                section += "  <td>{}</td>\n".format(versions[i])

            section += "  <td>{}</td>\n".format(self.burnin)
            section += "  <td>{}</td>\n".format(self.subsample)

            remaining = (self.run(i).n_iterations() - self.burnin)/self.subsample
            section += "  <td>{}</td>\n".format(remaining)

            if not all_same(commands):
                section += "  <td>{}</td>\n".format(commands[i])
            section += "  <td>{}</td>\n".format(self.run(i).get_dir())
            if not all_same(parent_dirs):
                section += "  <td>{}/td>\n".format(parent_dirs[i])

            section += "</tr>\n"

        section += "</table>\n"
        return section

    def section_model_and_priors(self):
        section = '<h2 style="clear:both"><a class="anchor" name="models"></a>Model and priors</h2>\n'

        section += '<h3 style="clear:both"><a class="anchor" name="tree"></a>Tree (+priors)</h3>\n'
#    $section .= "<table class=\"backlit2\">\n";
        section += "<table>\n"
        if self.get_topology_prior():
            section += '<tr><td class="modelname">topology</td><td>{}</td></tr>'.format(self.get_topology_prior())
        if self.get_branch_length_prior():
            section += '<tr><td class="modelname">branch lengths</td><td>{}</td></tr>'.format(self.get_branch_length_prior())
        section += '</table>\n'

        section += '<h3 style="clear:both"><a class="anchor" name="smodel"></a>Substitution model (+priors)</h3>\n'
        section += print_models("S",self.get_smodels())

        section += '<h3 style="clear:both"><a class="anchor" name="imodel"></a>Indel model (+priors)</h3>\n'
        section += print_models("I",self.get_imodels())

        if (self.get_scale_models()):
            section += '<h3 style="clear:both"><a class="anchor" name="scales"></a>Scales (+priors)</h3>\n'
            section += print_models("scale",self.get_scale_models());

        return section

    def section_end(self):
        return """\
     </div>
   </body>
</html>
"""


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Generate an HTML report summarizing MCMC runs for BAli-Phy and other software.",
                                     epilog= "Examples:\n   bp-analyze analysis-dir-1/ analysis-dir-2/\n   bp-analyze trees1.trees trees2.trees\n   bp-analyze log1.log log2.log",
                                     formatter_class=argparse.RawTextHelpFormatter)

    parser.add_argument("mcmc_outputs", default=['.'], help="Subdirectories with MCMC runs",nargs='*')
    parser.add_argument("--clean", default=False,help="Delete generated files",action='store_true')
    parser.add_argument("--verbose",default=0, help="Be verbose",action='store_true')
    parser.add_argument("--skip", metavar='NUM',type=int,default=None,help="Skip NUM iterations as burnin")
    parser.add_argument("--subsample",metavar='NUM',type=int,default=1,help="Keep only every NUM iterations")
    parser.add_argument("--until",type=int,default=None)
    parser.add_argument("--prune", default=None,help="Taxa to remove")
#    parser.add_argument("--muscle")
#    parser.add_argument("--probcons")
#    parser.add_argument("--mafft")
    parser.add_argument("--subpartitions",default=False,action='store_true')
    args = parser.parse_args()

    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)

    analysis = Analysis(args,args.mcmc_outputs)
