import collections
import copy
import logging
import os
import re
import warnings
from collections import Counter
from datetime import datetime, timezone
from random import shuffle
from typing import Any, Optional, Sequence

import numpy as np
import pkg_resources
import ruamel.yaml as yaml
import stickydesign as sd
import stickydesign.multimodel as multimodel
import stickydesign.stickydesign2 as sd2
import xgrow.tileset as xgt

# Need to disable this for now
from peppercompiler import compiler as compiler
from peppercompiler import finish as finish
from peppercompiler.design import spurious_design as spurious_design
from peppercompiler.DNA_classes import wc
from ruamel.yaml.comments import CommentedMap
from ruamel.yaml.representer import RoundTripRepresenter

from . import fastreduce, seeds
from . import sensitivitynew as sensitivity
from . import seq, tilestructures, util
from .ends import EndList
from .tiles import Tile, TileList
from .util import (
    DEFAULT_ENERGETICS,
    DEFAULT_MM_ENERGETICS_NAMES,
    DEFAULT_MULTIMODEL_ENERGETICS,
    DEFAULT_REGION_ENERGETICS,
    DEFAULT_SD2_MULTIMODEL_ENERGETICS,
)

SELOGGER = logging.getLogger(__name__)


class TileSet(CommentedMap):
    """A class representing a single DNA tile assembly system.

    This class parses and manipulates Alhambra tilesets.  Note that,
    unless you have already imported or made a dict, you will likely
    want to construct this with :meth:`TileSet.from_file`.

    Parameters
    ----------
    val : dict
        a dict in the Alhambra tileset format.
    """

    def __init__(self, val={}):
        CommentedMap.__init__(self, val)

        self.glues = EndList(self.get("ends", []))
        self["tiles"] = TileList(self.get("tiles", []))

    @classmethod
    def from_file(cls, name_or_stream, *args, **kwargs) -> "TileSet":
        """
        Class method to create a TileSet from a file or stream.

        Loads a YAML stream/file into TileSet.  This uses ruamel.yaml and tries to preserve
        comments, which is supported through `TileSet.to_file`.

        Parameters
        ----------
        name_or_stream : file-like or path-like object
            The path of the file, or a file object, to load.

        Returns
        -------
        TileSet
            The loaded TileSet.
        """
        # Assume a stream:
        if getattr(name_or_stream, "read", None) is None:
            return cls(yaml.round_trip_load(open(name_or_stream, "r"), *args, **kwargs))
        else:
            return cls(yaml.round_trip_load(name_or_stream, *args, **kwargs))

    def to_file(self, name_or_stream):
        """
        Write a TileSet to a file or stream.

        Parameters
        ----------
        name_or_stream : file-like or path-like object
            The path of the file, or a file object, to write to.

        """

        if getattr(name_or_stream, "write", None) is None:
            return yaml.round_trip_dump(self, open(name_or_stream, "w"))
        else:
            return yaml.round_trip_dump(self, name_or_stream)

    def create_sequence_diagrams(self, filename, *options):
        """Write sequence tile diagrams in SVG for the TileSet.

        Parameters
        ----------

        filename : str
            a path or filename for the SVG output.
        *options
            (currently unused)
        """

        import os.path

        import pkg_resources
        from lxml import etree

        base = etree.parse(
            pkg_resources.resource_stream(
                __name__, os.path.join("seqdiagrambases", "blank.svg")
            )
        )
        baseroot = base.getroot()
        pos = 150
        for tile in self.tiles:
            if tile.is_fake:
                continue
            group = tile.sequence_diagram()

            group.attrib["transform"] = "translate(0,{})".format(pos)
            pos += 150
            baseroot.append(group)

        base.write(filename)

    def create_adapter_sequences(self) -> "TileSet":
        """Generate adapter sequences for the TileSet, returning a new
        TileSet with the generated sequences included.

        Returns
        -------

        TileSet
           TileSet with generated adapter sequences.
        """
        seedclass = seeds.seedtypes[self.seed["type"]]
        if seedclass.needspepper:
            warnings.warn(
                "This set must have adapter sequences created during\
     regular sequence design. You can ignore this if you just created sequences."
            )
            return self
        return seedclass.create_adapter_sequences(self)

    @property
    def strand_order_list(self):
        """Return a list of orderable strand sequences for all tile strands.

        These sequences are generated by the Tile.orderableseqs attribute, which
        should include useful strand names and modified bases for labels.  The
        sequences may thus not be valid ACGT-only sequences (eg, they may contain
        /iBiodT/).

        Returns
        -------

        list
            a list of tuple (strand_name, strand_sequence) for the system.
        """
        return [y for x in self.tiles if not x.is_fake for y in x.orderableseqs]

    def dump(self, stream):
        """Dump the tileset into a stream in YAML format.

        Parameters
        ----------

        stream : stream
            the stream / file object to dump to.
        """

        return yaml.round_trip_dump(self, stream)

    def dx_design_set(
        tileset,
        name="tsd_temp",
        includes=[pkg_resources.resource_filename(__name__, "peppercomps-j1")],
        energetics=None,
        stickyopts={},
        reorderopts={},
        coreopts={},
        keeptemp=False,
    ):
        """Helper function to design sets from scratch, calling the numerous parts of
        tilesetdesigner. You may want to use the tilesetdesigner shell script
        instead.

        As with other functions in tilesetdesigner, this should not clobber inputs.

        Parameters
        ----------

        name : str, optional
            Base name for temporary files (default tsd_temp).

        Returns
        -------
        TileSet
            A copy of the TileSet with added sequences.

        """
        if energetics is None:
            energetics = DEFAULT_ENERGETICS

        if hasattr(tileset, "read"):
            tileset = TileSet.load(tileset)
        else:
            tileset = TileSet(tileset)

        tileset.check_consistent()
        tileset_with_ends_randomorder, new_ends = tileset.dx_create_end_sequences(
            energetics=energetics, **stickyopts
        )
        tileset_with_ends_ordered = tileset_with_ends_randomorder.reorder_ends(
            newends=new_ends, energetics=energetics, **reorderopts
        )
        tileset_with_strands = tileset_with_ends_ordered.create_strand_sequences(
            name, includes=includes, **coreopts
        )

        if "guards" in tileset_with_strands.keys():
            tileset_with_strands = (
                tileset_with_strands.dx_create_guard_strand_sequences()
            )

        # FIXME: this is temporary, until we have a better way of deciding.
        if "createseqs" in tileset_with_strands["seed"].keys():
            tileset_with_strands = tileset_with_strands.create_adapter_sequences()

        if not keeptemp:
            os.remove(name + ".fix")
            os.remove(name + ".mfe")
            os.remove(name + ".pil")
            os.remove(name + ".save")
            os.remove(name + ".seqs")
            os.remove(name + ".sys")

        tileset_with_strands.check_consistent()
        return tileset_with_strands

    def dx_create_end_sequences(
        self,
        method="default",
        energetics=None,
        trials=100,
        devmethod="dev",
        sdopts={},
        ecpars={},
        listends=False,
    ):
        """Create sticky end sequences for the TileSet, using stickydesign,
        and returning a new TileSet including the ends.


        Parameters
        ----------
        method: {'default', 'multimodel'}
            if 'default', use the default, single-model sequence design.
            If 'multimodel', use multimodel end choice.

        energetics : stickydesign.Energetics
            the energetics instance to use for the design, or list
            of energetics for method='multimodel', in which case the first
            will be the primary.  If None (default), will use
            alhambra.DEFAULT_ENERGETICS, or, if method='multimodel', will use
            alhambra.DEFAULT_MM_ENERGETICS.

        trials : int
            the number of trials to attempt. FIXME

        sdopts : dict
            a dictionary of parameters to pass to stickydesign.easy_ends.

        ecpars : dict
            a dictionary of parameters to pass to the endchooser function
            generator (useful only in limited circumstances).

        listends : bool
            if False, return just the TileSet.  If True, return both the
            TileSet and a list of the names of the ends that were created.

        Returns
        -------
        tileset : TileSet
            TileSet with designed end sequences included.
        new_ends : list
            Names of the ends that were designed.

        """
        info = {}
        info["method"] = method
        info["time"] = datetime.now(tz=timezone.utc).isoformat()
        info["sd_version"] = sd.version.__version__

        if not energetics:
            if method == "multimodel":
                energetics = DEFAULT_MULTIMODEL_ENERGETICS
            else:
                energetics = DEFAULT_ENERGETICS
        if method == "multimodel" and not isinstance(energetics, collections.Iterable):
            raise ValueError("Energetics must be an iterable for multimodel.")
        elif method == "multimodel":
            all_energetics = energetics
            energetics = all_energetics[0]
            info["energetics"] = [str(e) for e in all_energetics]
            info["trails"] = trials
        elif method == "default":
            info["energetics"] = str(energetics)

        # Steps for doing this:

        # Create a copy of the tileset.
        newtileset = self.copy()

        # Build a list of ends from the endlist in the tileset.  Do this
        # by creating a NamedList, then merging them into it.
        ends = EndList()

        if newtileset.glues:
            ends.update(newtileset.glues, fail_immediate=False)

        # This is the endlist from the tiles themselves.
        if newtileset.tiles:  # maybe you just want ends?
            # this checks for end/complement usage, and whether any
            # previously-describedends are unused
            # FIXME: implement
            # tilestructures.check_end_usage(newtileset.tiles, ends)

            endlist_from_tiles = newtileset.tiles.glues_from_tiles()

        ends.update(endlist_from_tiles)

        # Ensure that if there are any resulting completely-undefined ends, they
        # have their sequences removed.
        # for end in ends:
        #    if end.fseq and set(end.fseq) == {'n'}:
        #        del(end.fseq)

        # Build inputs suitable for stickydesign: lists of old sequences for TD/DT,
        # and numbers of new sequences needed.
        oldDTseqs = [
            end.fseq for end in ends if end.etype == "DT" and seq.is_definite(end.fseq)
        ]
        if oldDTseqs:
            oldDTarray = sd.endarray(oldDTseqs, "DT")
        oldTDseqs = [
            end.fseq for end in ends if end.etype == "TD" and seq.is_definite(end.fseq)
        ]
        if oldTDseqs:
            oldTDarray = sd.endarray(oldTDseqs, "TD")

        newTD = [
            end for end in ends if end.etype == "TD" and not seq.is_definite(end.fseq)
        ]
        newDT = [
            end for end in ends if end.etype == "DT" and not seq.is_definite(end.fseq)
        ]

        # Deal with energetics, considering potential old sequences.
        # FIXME: EXPLAIN WHAT THIS ABSTRUSE CODE DOES...
        # TODO: tests needs to test this
        targets = []
        if len(oldDTseqs) == 0 and len(oldTDseqs) == 0:
            targets.append(sd.enhist("DT", 5, energetics=energetics)[2]["emedian"])
            targets.append(sd.enhist("TD", 5, energetics=energetics)[2]["emedian"])
        if len(oldDTseqs) > 0:
            targets.append(energetics.matching_uniform(oldDTarray))
        if len(oldTDseqs) > 0:
            targets.append(energetics.matching_uniform(oldTDarray))
        targetint = np.average(targets)

        if any(not seq.is_null(end.fseq) for end in newTD):
            TDtemplates = [end.fseq for end in newTD]
        else:
            TDtemplates = None
        if any(not seq.is_null(end.fseq) for end in newDT):
            DTtemplates = [end.fseq for end in newDT]
        else:
            DTtemplates = None

        if method == "default":
            if TDtemplates or DTtemplates:
                raise NotImplementedError
            # Create new sequences.
            newTDseqs = sd.easyends(
                "TD",
                5,
                number=len(newTD),
                energetics=energetics,
                interaction=targetint,
                **sdopts,
            ).tolist()

            newDTseqs = sd.easyends(
                "DT",
                5,
                number=len(newDT),
                energetics=energetics,
                interaction=targetint,
                **sdopts,
            ).tolist()

        elif method == "multimodel":
            SELOGGER.info(
                "starting multimodel sticky end generation "
                + "of TD ends for {} DT and {} TD ends, {} trials.".format(
                    len(newDT), len(newTD), trials
                )
            )

            newTDseqs = []
            pl = util.ProgressLogger(SELOGGER, trials * 2)
            presetavail = None
            for i in range(0, trials):
                endchooserTD = multimodel.endchooser(
                    all_energetics, templates=TDtemplates, devmethod=devmethod, **ecpars
                )

                e, presetavail = sd.easyends(
                    "TD",
                    5,
                    number=len(newTD),
                    oldends=oldTDseqs,
                    energetics=energetics,
                    interaction=targetint,
                    echoose=endchooserTD,
                    _presetavail=presetavail,
                    **sdopts,
                )
                newTDseqs.append(e)
                pl.update(i)

            if oldTDseqs:
                tvals = [
                    [e.matching_uniform(oldTDarray[0:1]) for e in all_energetics]
                    * len(newTDseqs)
                ] * len(newTDseqs)
                SELOGGER.debug(tvals[0])
            else:
                tvals = [
                    [e.matching_uniform(x[0:1]) for e in all_energetics]
                    for x in newTDseqs
                ]

            endchoosersDT = [
                multimodel.endchooser(
                    all_energetics,
                    target_vals=tval,
                    templates=DTtemplates,
                    devmethod=devmethod,
                    **ecpars,
                )
                for tval in tvals
            ]

            SELOGGER.info("generating corresponding DT ends")
            newDTseqs = []
            presetavail = None

            for i, echoose in enumerate(endchoosersDT):
                e, presetavail = sd.easyends(
                    "DT",
                    5,
                    number=len(newDT),
                    oldends=oldDTseqs,
                    energetics=energetics,
                    interaction=targetint,
                    echoose=echoose,
                    _presetavail=presetavail,
                    **sdopts,
                )
                newDTseqs.append(e)

                pl.update(i + trials)

            arr = [
                [
                    sd.endarray(oldTDseqs + x.tolist(), "TD"),
                    sd.endarray(oldDTseqs + y.tolist(), "DT"),
                ]
                for x, y in zip(newTDseqs, newDTseqs)
            ]

            scores = [
                multimodel.deviation_score(list(e), all_energetics, devmethod=devmethod)
                for e in arr
            ]

            sort = np.argsort(scores)

            newTDseqs = newTDseqs[sort[0]].tolist()[len(oldTDseqs) :]
            newDTseqs = newDTseqs[sort[0]].tolist()[len(oldDTseqs) :]
            info["score"] = float(scores[sort[0]])
            info["maxscore"] = float(scores[sort[-1]])
            info["meanscore"] = float(np.mean(scores))

        # FIXME: move to stickydesign
        assert len(newTDseqs) == len(newTD)
        assert len(newDTseqs) == len(newDT)

        # Shuffle the lists of end sequences, to ensure that they're
        # random order, and that ends used earlier in the set are not
        # always better than those used later. But only shuffle if
        # there were no templates:
        if not TDtemplates:
            shuffle(newTDseqs)
        if not DTtemplates:
            shuffle(newDTseqs)

        # Make sure things are consistent if there are templates:
        if TDtemplates:
            for t, s in zip(TDtemplates, newTDseqs):
                seq.merge(t, s)
        if DTtemplates:
            for t, s in zip(DTtemplates, newDTseqs):
                seq.merge(t, s)

        for end, s in zip(newDT, newDTseqs):
            ends[end.name].fseq = s
        for end, s in zip(newTD, newTDseqs):
            ends[end.name].fseq = s

        ends.check_consistent()

        # Ensure that the old and new sets have consistent end definitions,
        # and that the tile definitions still fit.
        self.glues.merge(ends)
        newtileset.tiles.glues_from_tiles().merge(ends)

        newendnames = [e.name for e in newTD] + [e.name for e in newDT]
        info["newends"] = newendnames

        # Apply new sequences to tile system.
        newtileset.glues = ends
        if "info" not in newtileset.keys():
            newtileset["info"] = {}
        if "end_design" not in newtileset["info"].keys():
            newtileset["info"]["end_design"] = []
        if isinstance("end_design", dict):  # convert old
            newtileset["info"]["end_design"] = [newtileset["info"]["end_design"]]
        newtileset["info"]["end_design"].append(info)

        return newtileset, newendnames

    def _add_info(self, ititle, data):
        if "info" not in self.keys():
            self["info"] = {}
        if ititle not in self["info"].keys():
            self["info"][ititle] = []
        self["info"][ititle].append(data)

    def reorder_ends(
        tileset,
        newends=[],
        hightemp=0.1,
        lowtemp=1e-7,
        steps=45000,
        update=1000,
        energetics=None,
        fsopts={},
    ):
        """Given a tileset dictionary that includes sticky end sequences, reorder these
        to try to optimize error rates.
        """
        from . import anneal, endreorder

        if energetics is None:
            energetics = DEFAULT_ENERGETICS

        tset = tileset.copy()

        if "info" not in tset.keys():
            tset["info"] = {}

        reordersys = endreorder.EndSystemFseq(
            tset, newends, energetics=energetics, **fsopts
        )

        # FIXME: better parameter control here.
        annealer = anneal.Annealer(reordersys.score, reordersys.mutate)

        newstate = annealer.anneal(
            reordersys.initstate, hightemp, lowtemp, steps, update
        )

        # Now take that new state, and apply it to the new tileset.
        seqs = reordersys.slowseqs(newstate[0])
        for end in tset.glues:
            if end.etype in ["DT", "TD"]:
                eloc = reordersys.enlocs[end["name"]]
                end.fseq = seqs[eloc[1]].tolist()[eloc[0]]

        ri = {}

        ri["score"] = float(reordersys.score(newstate[0]))

        tset["info"]["reorder"] = ri

        # Ensure that only ends in newends moved: that all others remain mergeable:
        if newends:
            old_ends_from_new_set = EndList(
                end for end in tset.glues if end["name"] not in newends
            )
            tileset.glues.merge(old_ends_from_new_set)

        # Ensure system consistency
        tset.check_consistent()
        return tset

    def create_strand_sequences(
        tileset,
        basename="alhambratemp",
        includes=[pkg_resources.resource_filename(__name__, "peppercomps-j1")],
        spurious_pars="verboten_weak=1.5",
        *options,
    ):
        """Given a tileset dictionary with sticky ends sequences, create core sequences
        for tiles, using Pepper.

        Parameters
        ----------

        basename : str, optional
            The base name to use in Pepper (FIXME: why is this needed?).

        includes : list of str paths, optional
            The include paths for Pepper.

        spurious_pars : str
            Command line options for SpuriousDesign

        Returns
        -------

        TileSet
            A TileSet with generated strand sequences for all tiles.
        """

        if not basename:
            import uuid

            basename = str(uuid.uuid4()).replace("-", "")
        # FIXME: is this a valid basename?

        newtileset = copy.deepcopy(tileset)

        newtileset._create_pepper_input_files(basename)

        compiler.compiler(
            basename,
            [],
            basename + ".pil",
            basename + ".save",
            fixed_file=basename + ".fix",
            includes=includes,
            synth=True,
        )

        spurious_design.design(
            basename,
            infilename=basename + ".pil",
            outfilename=basename + ".mfe",
            verbose=True,
            struct_orient=True,
            tempname=basename + "-temp",
            extra_pars=spurious_pars,
            findmfe=False,
            cleanup=False,
        )

        if "info" not in newtileset.keys():
            newtileset["info"] = {}
        if "core" not in newtileset["info"].keys():
            newtileset["info"]["core"] = []
        elif isinstance(newtileset["info"]["core"], dict):
            newtileset["info"]["core"] = [newtileset["info"]["core"]]

        with open(basename + "-temp.sp") as f:
            a = f.read()
            cdi = {}
            cdi["basename"] = basename
            cdi["score_verboten"] = float(
                re.findall(r"score_verboten\s+score\s+=\s+([+-]?[\d.,]+)", a)[1]
            )
            cdi["score_spurious"] = float(
                re.findall(r"score_spurious\s+score\s+=\s+([+-]?[\d.,]+)", a)[1]
            )
            cdi["score_bonds"] = float(
                re.findall(r"score_bonds\s+score\s+=\s+([+-]?[\d.,]+)", a)[1]
            )
            cdi["score"] = float(
                re.findall(r"weighted score\s+=\s+([+-]?[\d.,]+)", a)[1]
            )
            cdi["spurious_output"] = re.search(
                r"(?<=FINAL\n\n)[\w\W]+weighted.*", a, re.MULTILINE
            ).group(0)

        cdi["time"] = datetime.now(tz=timezone.utc).isoformat()
        newtileset["info"]["core"].append(cdi)

        finish.finish(
            basename + ".save",
            designname=basename + ".mfe",
            seqsname=basename + ".seqs",
            strandsname=None,
            run_kin=False,
            cleanup=False,
            trials=0,
            time=0,
            temp=27,
            conc=1,
            spurious=False,
            spurious_time=0,
        )  # FIXME: shouldn't need so many options.

        tileset_with_strands = newtileset._load_pepper_output_files(basename)

        # Ensure:
        tileset.glues.merge(
            tileset_with_strands.tiles.glues_from_tiles()
        )  # Ends still fit
        for tile in tileset_with_strands.tiles:
            oldtile = tileset.tiles[tile.name]
            if "fullseqs" in oldtile.keys():
                for old, new in zip(oldtile["fullseqs"], tile["fullseqs"]):
                    seq.merge(old, new)  # old tile sequences remain
            assert oldtile.ends == tile.ends

        # Check that old end sequences remain
        tileset.glues.merge(tileset_with_strands.glues)

        return tileset_with_strands

    def _create_pepper_input_files(tileset, basename):
        # Are we creating adapters in Pepper?
        if tileset.seed and seeds.seedtypes[tileset["seed"]["type"]].needspepper:
            seedclass = seeds.seedtypes[tileset["seed"]["type"]]
            createadapts = True
        else:
            createadapts = False

        fixedfile = open(basename + ".fix", "w")
        # We first need to create a fixed sequence list/file for pepper.
        # Add fixed sticky end and adjacent tile sequences.
        for end in tileset.glues:
            if "fseq" not in end.keys():
                continue
            seq = end["fseq"][1:-1]
            if end["type"] == "TD":
                adj = end["fseq"][-1]
                cadj = end["fseq"][0]  # FIXME: WAS [1], OFF BY ONE!
            elif end["type"] == "DT":
                adj = end["fseq"][0]
                cadj = end["fseq"][-1]  # FIXME: WAS [1], OFF BY ONE!
            else:
                print("warning! end {} not recognized".format(end["name"]))
            fixedfile.write("signal e_{0} = {1}\n".format(end["name"], seq.upper()))
            fixedfile.write("signal a_{0} = {1}\n".format(end["name"], adj.upper()))
            fixedfile.write("signal c_{0} = {1}\n".format(end["name"], cadj.upper()))
            # If we are creating adapter tiles in Pepper, add origami-determined
            # sequences
        if createadapts:
            for i, core in enumerate(seedclass.cores, 1):
                fixedfile.write("signal origamicore_{0} = {1}\n".format(i, core))

        # Now we'll create the system file in parts.
        importlist = set()
        compstring = ""

        for tile in tileset.tiles:
            if tile.is_fake:
                continue
            e = [[], []]
            for end in tile.ends:
                if end == "hp":
                    continue
                    # skip hairpins, etc that aren't designed by stickydesign
                e[0].append("e_" + end.replace("/", "*"))
                if end[-1] == "/":
                    a = "c_" + end[:-1] + "*"
                else:
                    a = "a_" + end
                e[1].append(a)
            s1 = " + ".join(e[0])
            s2 = " + ".join(e[1])
            tiletype = tile["structure"]
            if "extra" in tile.keys():
                tiletype += "_" + tile["extra"]
            compstring += "component {} = {}: {} -> {}\n".format(
                tile["name"], tiletype, s1, s2
            )
            importlist.add(tiletype)
            if "fullseqs" in tile.keys():
                fixedfile.write(
                    "structure {}-tile = ".format(tile["name"])
                    + "+".join([seq.upper() for seq in tile["fullseqs"]])
                    + "\n"
                )

        if createadapts:
            importlist, compstring = seedclass._create_pepper_input_files(
                tileset["seed"], importlist, compstring
            )

        with open(basename + ".sys", "w") as sysfile:
            sysfile.write("declare system {}: ->\n\n".format(basename))
            sysfile.write("import " + ", ".join(importlist) + "\n\n")
            sysfile.write(compstring)

    def _load_pepper_output_files(tileset, basename):
        import re

        # Are we creating adapters in Pepper?
        # if seeds.seedtypes[tileset['seed']['type']].needspepper:
        #     seedclass = seeds.seedtypes[tileset['seed']['type']]
        #     createadapts = True

        tset = copy.deepcopy(tileset)

        seqsstring = open(basename + ".seqs").read()

        for tile in tset.tiles:
            if tile.is_fake:
                continue
            pepperstrands = re.compile(
                "strand " + tile["name"] + "-([^ ]+) = ([^\n]+)"
            ).findall(seqsstring)
            tile["fullseqs"] = tilestructures.order_pepper_strands(pepperstrands)

        # for adapter in tset['seed']['adapters']:
        #    pepperstrands = re.compile('strand ' + adapter['name'] +
        #                               '-([^ ]+) = ([^\n]+)').findall(
        #                                   seqsstring)
        #    adapter['fullseqs'] = tilestructures.order_pepper_strands(
        #        pepperstrands)

        return tset

    def dx_create_guard_strand_sequences(tileset):
        """Given a tileset dictionary with core tile sequences,
        create guard strand sequences.

        Returns
        -------

        TileSet
            A TileSet with generated guard strand sequences.
        """
        tset = tileset.copy()

        for guard in tset["guards"]:
            tile = tset.tiles[guard[0]]
            guard.append(wc(tile["fullseqs"][guard[1] - 1]))

        return tset

    def dx_create_adapter_sequence_diagrams(tileset, filename, *options):
        """Create sequence diagrams of adapters for the seed."""
        import os.path

        import pkg_resources
        from lxml import etree

        base = etree.parse(
            pkg_resources.resource_stream(
                __name__, os.path.join("seqdiagrambases", "blank.svg")
            )
        )
        baseroot = base.getroot()
        pos = 200
        for adapterdef in tileset["seed"]["adapters"]:

            seedclass = seeds.seedtypes[tileset["seed"]["type"]]
            group = seedclass.create_adapter_sequence_diagram(adapterdef)

            group.attrib["transform"] = "translate(0,{})".format(pos)
            pos += 200
            baseroot.append(group)

        base.write(filename)

    def run_xgrow(
        self,
        perfect=False,
        rotate=False,
        energetics=None,
        output=None,
        labelsonly=False,
        onlyreal=True,
        **xgrowparams,
    ):
        """Run Xgrow for the system.

        Parameters
        ----------

        xgrowparams : dict, optional
            Extra Xgrow parameters.  For example, to change Gse and use 2px block, use
        {'Gse': 9.2, 'block': 2}.

        perfect : bool
            If True, each end binds to its complement by strength 1.  If False,
            stickydesign energetics are used.  Note that TileSets with odd ends
            (eg, with different "ends" that are actually just the same end with
            different adjacent bases, as in the original COUNT system) will likely
            not behave as expected with perfect=True.

        rotate : bool, optional
            If False (Default), tiles are simply converted to Xgrow tiles.  If True,
            rotated/flipped copies of each tile are created in order to simulate valid
            tile orientations other than those intended.

        energetics : stickydesign.Energetics, optional
            The energetics model to use.  If not specified, DEFAULT_ENERGETICS is used.


        output : str or list, optional
            either a string or list, specifying output options.  If a
            string, one of 'final', 'array', or 'trace', corresponding to 'datafile',
            'arrayfile' and 'tracefile', respectively.  If a list of multiple, then
            do those.  These will manage the output, and return the data in usable
            form.

        Returns
        -------
        list or various
            data as requested from output.  See xgrow.run for more information.

        """
        import xgrow

        return xgrow.run(
            self.generate_xgrow_dict(
                perfect=perfect,
                rotate=rotate,
                energetics=energetics,
                labelsonly=labelsonly,
                onlyreal=onlyreal,
            ),
            outputopts=output,
            **xgrowparams,
        )

    # FIXME: NEED TO IMPLEMENT THIS WITH BETTER CODE
    # def sensitivity_classes(ts, count=False, _maxorder=2):
    #    return sensitivity.sensitivity_classes(
    #        ts, count=False, _maxorder=_maxorder)

    def generate_xgrow_dict(
        self,
        perfect: bool = False,
        rotate: bool = False,
        energetics=None,
        labelsonly: bool = False,
        onlyreal: bool = True,
    ):
        """Generate a Xgrow tileset dict.

        Parameters
        ----------

        perfect : bool
            If True, each end binds to its complement by strength 1.  If False,
            stickydesign energetics are used.  Note that TileSets with odd ends
            (eg, with different "ends" that are actually just the same end with
            different adjacent bases, as in the original COUNT system) will likely
            not behave as expected with perfect=True.

        rotate : bool, optional
            If False (Default), tiles are simply converted to Xgrow tiles.  If True,
            rotated/flipped copies of each tile are created in order to simulate valid
            tile orientations other than those intended.

        energetics : stickydesign.Energetics, optional
            The energetics model to use.  If not specified, DEFAULT_ENERGETICS is used.

        Returns
        -------

        dict
            An xgrow-compatible tileset definition, suitable for, eg, xgrow.run.
        """

        # Combine ends and tile-specified adjacents
        newtiles = []
        newends = []
        doubleends = []
        doubles = []
        vdoubleends = []
        vdoubles = []
        ts = copy.deepcopy(self)
        if ts.seed:
            seedtype = seeds.seedtypes[ts.seed["type"]]
            newtiles.append(
                {
                    "name": "origami",
                    "edges": ["origami", "origami", "origami", "origami"],
                    "stoic": 0,
                    "color": "white",
                }
            )

            atiles = [None] * len(seedtype.cores)
            to_use = []
            # If we have use_adapters, use that, otherwise use every adapter:
            if "use_adapters" in ts["seed"]:
                for tilename in ts["seed"]["use_adapters"]:
                    try:
                        tile = [
                            x
                            for x in ts["seed"]["adapters"]
                            if x.get("name") == tilename
                        ][0]
                        to_use.append(tile)
                    except IndexError as e:
                        raise Exception("Can't find {}".format(tilename)) from e
            else:
                to_use = ts["seed"]["adapters"]

            for tile in to_use:
                newtile: dict[str, Any] = {}
                if "ends" in tile.keys():
                    newtile["edges"] = (
                        ["origami"]
                        + [re.sub("/", "_c", x) for x in tile.ends]
                        + ["origami"]
                    )
                else:
                    mtile = ts.tiles[tile["tilebase"]]
                    newtile["edges"] = (
                        ["origami"]
                        + [
                            re.sub("/", "_c", x)
                            for x in mtile.ends[
                                seedtype._mimicadapt[mtile.structure.name].ends
                            ]
                        ]
                        + ["origami"]
                    )
                newtile["name"] = tile.get("name", "")
                newtile["stoic"] = 0
                newtile["color"] = "white"
                atiles[tile["loc"] - 1] = newtile
            for tile in atiles:
                if tile:
                    newtiles.append(tile)
                else:
                    newtiles.append(
                        {
                            "name": "emptyadapt",
                            "edges": ["origami", 0, 0, "origami"],
                            "stoic": 0,
                            "color": "white",
                        }
                    )

        if onlyreal:
            ts.tiles = TileList([x for x in ts.tiles if "fake" not in x])

        if rotate:
            rotatedtiles = []
            for tile in ts.tiles:
                # only include rotated tiles that aren't identical (handles symmetric tiles)
                trot = [t for t in tile.rotations if (t.ends != tile.ends)]
                td = []
                for i, tr in enumerate(trot):
                    for j in range(0, i):
                        if tr.ends == trot[j].ends:
                            td.append(i)
                            break
                for i in reversed(td):
                    del trot[i]
                rotatedtiles += trot
            ts.tiles += rotatedtiles

        for tile in ts.tiles:
            if "rotation" in tile.keys():
                tile["name"] = tile["name"] + "_rot{}".format(tile["rotation"])
            if re.match("tile_daoe_3up", tile.structure.name) or re.match(
                "tile_daoe_5up", tile.structure.name
            ):
                newtile = Tile()
                newtile["edges"] = [re.sub("/", "_c", x) for x in tile.ends]
                if "name" in tile:
                    newtile["name"] = tile["name"]
                if "conc" in tile:
                    newtile["stoic"] = tile["conc"]
                if ("color" in tile) and (not labelsonly):
                    newtile["color"] = tile["color"]
                elif labelsonly:
                    if "label" in tile.keys():
                        newtile["color"] = "white"
                    else:
                        newtile["color"] = "gray50"
                newtiles.append(newtile)

            if re.match("tile_daoe_doublehoriz", tile.structure.name):
                newtile1: dict[str, Any] = {}
                newtile2: dict[str, Any] = {}
                newtile1["edges"] = (
                    [re.sub("/", "_c", x) for x in tile.ends[0:1]]
                    + [tile["name"] + "_db"]
                    + [re.sub("/", "_c", x) for x in tile.ends[4:]]
                )
                newtile2["edges"] = [re.sub("/", "_c", x) for x in tile.ends[1:4]] + [
                    tile["name"] + "_db"
                ]
                newtile1["name"] = tile["name"] + "_left"
                newtile2["name"] = tile["name"] + "_right"

                doubleends.append(tile["name"] + "_db")
                doubles.append((newtile1["name"], newtile2["name"]))

                if "conc" in tile:
                    newtile1["stoic"] = tile["conc"]
                    newtile2["stoic"] = tile["conc"]

                if ("color" in tile) and (not labelsonly):
                    newtile1["color"] = tile["color"]
                    newtile2["color"] = tile["color"]
                elif labelsonly:
                    if "label" in tile.keys():
                        newtile1["color"] = "white"
                        newtile2["color"] = "white"
                    else:
                        newtile1["color"] = "gray50"
                        newtile2["color"] = "gray50"

                newtiles.append(newtile1)
                newtiles.append(newtile2)
            if re.match("tile_daoe_doublevert", tile.structure.name):
                newtile1 = {}
                newtile2 = {}
                newtile1["edges"] = (
                    [re.sub("/", "_c", x) for x in tile.ends[0:2]]
                    + [tile["name"] + "_db"]
                    + [re.sub("/", "_c", x) for x in tile.ends[5:]]
                )
                newtile2["edges"] = [tile["name"] + "_db"] + [
                    re.sub("/", "_c", x) for x in tile.ends[2:5]
                ]
                newtile1["name"] = tile["name"] + "_top"
                newtile2["name"] = tile["name"] + "_bottom"

                vdoubleends.append(tile["name"] + "_db")
                vdoubles.append((newtile1["name"], newtile2["name"]))

                if "conc" in tile:
                    newtile1["stoic"] = tile["conc"]
                    newtile2["stoic"] = tile["conc"]

                if ("color" in tile) and (not labelsonly):
                    newtile1["color"] = tile["color"]
                    newtile2["color"] = tile["color"]
                elif labelsonly:
                    if "label" in tile.keys():
                        newtile1["color"] = "white"
                        newtile2["color"] = "white"
                    else:
                        newtile1["color"] = "gray50"
                        newtile2["color"] = "gray50"

                newtiles.append(newtile1)
                newtiles.append(newtile2)

        newends.append({"name": "origami", "strength": 100})

        for end in doubleends:
            newends.append({"name": end, "strength": 10})
        for end in vdoubleends:
            newends.append({"name": end, "strength": 10})

        # check for whether we need to use perfect:
        if not perfect:
            for end in ts.allglues:
                if (end["type"] in {"TD", "DT"}) and "fseq" not in end.keys():
                    perfect = True
                    SELOGGER.warn(
                        "setting perfect=True, because {} has no sequence.".format(
                            end["name"]
                        )
                    )
                    break

        gluelist = []
        if not perfect:
            glueends = {"DT": [], "TD": []}
            for end in ts.allglues:
                newends.append({"name": end["name"], "strength": 0})
                newends.append({"name": end["name"] + "_c", "strength": 0})
                if (end["type"] == "TD") or (end["type"] == "DT"):
                    glueends[end["type"]].append((end["name"], end["fseq"]))

            if energetics:
                ef = energetics
            else:
                ef = DEFAULT_ENERGETICS

            eavg = {}
            for t in ["DT", "TD"]:
                names, fseqs = zip(*glueends[t])
                ea = sd.endarray(fseqs, t)
                eavg[t] = np.average(ef.matching_uniform(ea))
            eavg_combined = (eavg["DT"] + eavg["TD"]) / 2.0

            for t in ["DT", "TD"]:
                names, fseqs = zip(*glueends[t])
                allnames = names + tuple(x + "_c" for x in names)
                ea = sd.endarray(fseqs, t)
                ar = sd.energy_array_uniform(ea, ef) / eavg_combined
                for i1, n1 in enumerate(names):
                    for i2, n2 in enumerate(allnames):
                        gluelist.append([n1, n2, max(float(ar[i1, i2]), 0.0)])

        else:
            if "ends" not in ts.keys():
                ts.glues = []
            endsinlist = set(e["name"] for e in ts.glues)
            endsintiles = set()
            for tile in ts.tiles:
                endsintiles.update(re.sub("/", "", e) for e in tile.ends if e != "hp")
            for end in ts.glues + list({"name": e} for e in endsintiles):
                newends.append({"name": end["name"], "strength": 0})
                newends.append({"name": end["name"] + "_c", "strength": 0})
                gluelist.append([end["name"], end["name"] + "_c", 1.0])

        newends.append({"name": "hp", "strength": 0})

        xga = {}
        xga["doubletiles"] = [list(x) for x in doubles]
        xga["vdoubletiles"] = [list(x) for x in vdoubles]

        xga.update(ts.get("xgrow_options", dict()))

        # if not perfect:
        #    xga['gse_calc_avg'] = eavg_combined

        sts = {"tiles": newtiles, "bonds": newends, "xgrowargs": xga, "glues": gluelist}

        return sts
