#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020-2021  The SymbiFlow Authors.
#
# Use of this source code is governed by a ISC-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/ISC
#
# SPDX-License-Identifier: ISC


"""
This file for packing tracks into channels.

It does *not* manage channel XML nodes.

Note channels go between switchboxes. Switchboxes cannot be at the last grid
coordinate.

Therefore, you need a grid size of at least 3 rows or cols to allow any
channels to exist. With a 3 width configuration you would get a channel of
length 0, connecting the switchbox at 0 to the switchbox at 1.

With above in mind, objects here entirely omit the last row/col and placing a
channel in the first is illegal.

Specifically:
 * For CHANX: X=0 is invalid, X=grid.width-1 is invalid
 * For CHANY: Y=0 is invalid, Y=grid.height-1 is invalid
"""

import pprint
import enum
import io
from collections import namedtuple
import lxml.etree as ET

from . import Position
from . import Size

from . import node_pos, single_element
from .utils.asserts import assert_eq
from .utils.asserts import assert_len_eq
from .utils.asserts import assert_type
from .utils.asserts import assert_type_or_none


# FIXME: doctests and static_property are not playing nice together.
# from . import static_property
static_property = property


class ChannelNotStraight(TypeError):
    pass


_Track = namedtuple(
    "Track", ("start", "end", "direction", "segment_id", "idx")
)


class Track(_Track):
    """
    Represents a single CHANX or CHANY (track) within a channel.

    IE The tracks of a x_list or y_list entry in <channels> element.

    start: start Pos
    end: end Pos
    idx: XML index integer
    """

    class Type(enum.Enum):
        """
        subset of NodeType in graph2
        """
        # Horizontal routing
        X = 'CHANX'
        # Vertical routing
        Y = 'CHANY'

        def __repr__(self):
            return 'Track.Type.' + self.name

    class Direction(enum.Enum):
        INC = 'INC_DIR'
        DEC = 'DEC_DIR'
        BI = 'BI_DIR'

        def __repr__(self):
            return 'Track.Direction.' + self.name

    def __new__(
            cls,
            start,
            end,
            direction=Direction.INC,
            segment_id=0,
            idx=None,
            name=None,
            type_hint=None,
    ):
        """Make most but not all attributes immutable"""

        if not isinstance(start, Position):
            start = Position(*start)
        assert_type(start, Position)
        if not isinstance(end, Position):
            end = Position(*end)
        assert_type(end, Position)

        if start.x != end.x and start.y != end.y:
            raise ChannelNotStraight(
                "Track not straight! {}->{}".format(start, end)
            )

        assert_type(direction, cls.Direction)
        assert_type(segment_id, int)
        assert_type_or_none(idx, int)
        assert_type_or_none(name, str)
        assert_type_or_none(type_hint, cls.Type)

        obj = _Track.__new__(cls, start, end, direction, segment_id, idx)
        obj.name = name
        obj.type_hint = type_hint
        return obj

    @static_property
    def type(self):
        """Type of the channel.

        Returns: Track.Type

        >>> Track((1, 0), (10, 0)).type
        Track.Type.X
        >>> Track((0, 1), (0, 10)).type
        Track.Type.Y
        >>> Track((1, 1), (1, 1)).type
        Traceback (most recent call last):
            ...
        ValueError: Ambiguous type
        >>> Track((1, 1), (1, 1), type_hint=Track.Type.X).type
        Track.Type.X
        >>> Track((1, 1), (1, 1), type_hint=Track.Type.Y).type
        Track.Type.Y

        """
        if self.type_hint:
            return self.type_hint
        guess = self.type_guess
        if guess is None:
            raise ValueError("Ambiguous type")
        return guess

    @static_property
    def type_guess(self):
        """Type of the channel.

        Returns: Track.Type

        >>> Track((1, 0), (10, 0)).type_guess
        Track.Type.X
        >>> Track((0, 1), (0, 10)).type_guess
        Track.Type.Y
        >>> str(Track((1, 1), (1, 1)).type_guess)
        'None'
        """
        if self.start.x == self.end.x and self.start.y == self.end.y:
            return None
        elif self.start.x == self.end.x:
            return Track.Type.Y
        elif self.start.y == self.end.y:
            return Track.Type.X
        else:
            assert False, self

    def positions(self):
        """Generate all positions this track occupies"""
        startx, endx = sorted([self.start.x, self.end.x])
        starty, endy = sorted([self.start.y, self.end.y])

        for x in range(startx, endx + 1):
            for y in range(starty, endy + 1):
                yield Position(x, y)

    @static_property
    def start0(self):
        """The non-constant start coordinate.

        >>> Track((1, 0), (10, 0)).start0
        1
        >>> Track((0, 1), (0, 10)).start0
        1
        >>> Track((1, 1), (1, 1)).start0
        Traceback (most recent call last):
            ...
        ValueError: Ambiguous type
        >>> Track((1, 1), (1, 1), type_hint=Track.Type.X).start0
        1
        >>> Track((10, 0), (1, 0)).start0
        10
        >>> Track((0, 10), (0, 1)).start0
        10
        """
        if self.type == Track.Type.X:
            return self.start.x
        elif self.type == Track.Type.Y:
            return self.start.y
        else:
            assert False

    @static_property
    def end0(self):
        """The non-constant end coordinate.

        >>> Track((1, 0), (10, 0)).end0
        10
        >>> Track((0, 1), (0, 10)).end0
        10
        >>> Track((1, 1), (1, 1)).end0
        Traceback (most recent call last):
            ...
        ValueError: Ambiguous type
        >>> Track((1, 1), (1, 1), type_hint=Track.Type.X).end0
        1
        >>> Track((10, 0), (1, 0)).end0
        1
        >>> Track((0, 10), (0, 1)).end0
        1
        """
        if self.type == Track.Type.X:
            return self.end.x
        elif self.type == Track.Type.Y:
            return self.end.y
        else:
            assert False, self.type

    @static_property
    def common(self):
        """The common coordinate value.

        >>> Track((0, 0), (10, 0)).common
        0
        >>> Track((0, 0), (0, 10)).common
        0
        >>> Track((1, 1), (1, 1)).common
        Traceback (most recent call last):
            ...
        ValueError: Ambiguous type
        >>> Track((1, 1), (1, 1), type_hint=Track.Type.X).common
        1
        >>> Track((10, 0), (0, 0)).common
        0
        >>> Track((0, 10), (0, 0)).common
        0
        >>> Track((4, 10), (4, 0)).common
        4
        """
        if self.type == Track.Type.X:
            assert_eq(self.start.y, self.end.y)
            return self.start.y
        elif self.type == Track.Type.Y:
            assert_eq(self.start.x, self.end.x)
            return self.start.x
        else:
            assert False

    @static_property
    def length(self):
        """Length of the track.

        >>> Track((0, 0), (10, 0)).length
        10
        >>> Track((0, 0), (0, 10)).length
        10
        >>> Track((1, 1), (1, 1)).length
        0
        >>> Track((10, 0), (0, 0)).length
        10
        >>> Track((0, 10), (0, 0)).length
        10
        """
        try:
            return abs(self.end0 - self.start0)
        except ValueError:
            return 0

    def new_idx(self, idx):
        """Create a new channel with the same start/end but new index value.

        >>> s = (1, 4)
        >>> e = (1, 8)
        >>> c1 = Track(s, e, idx=0)
        >>> c2 = c1.new_idx(2)
        >>> assert_eq(c1.start, c2.start)
        >>> assert_eq(c1.end, c2.end)
        >>> c1.idx
        0
        >>> c2.idx
        2
        """
        return self.__class__(
            self.start,
            self.end,
            self.direction,
            self.segment_id,
            idx,
            name=self.name,
            type_hint=self.type_hint,
        )

    def __repr__(self):
        """

        >>> repr(Track((0, 0), (10, 0)))
        'T((0,0), (10,0))'
        >>> repr(Track((0, 0), (0, 10)))
        'T((0,0), (0,10))'
        >>> repr(Track((1, 2), (3, 2), idx=5))
        'T((1,2), (3,2), 5)'
        >>> repr(Track((1, 2), (3, 2), name="ABC"))
        'T(ABC)'
        >>> repr(Track((1, 2), (3, 2), idx=5, name="ABC"))
        'T(ABC,5)'
        """
        if self.name:
            idx_str = ""
            if self.idx is not None:
                idx_str = ",{}".format(self.idx)
            return "T({}{})".format(self.name, idx_str)

        idx_str = ""
        if self.idx is not None:
            idx_str = ", {}".format(self.idx)
        return "T(({},{}), ({},{}){})".format(
            self.start.x, self.start.y, self.end.x, self.end.y, idx_str
        )

    def __str__(self):
        """

        >>> str(Track((0, 0), (10, 0)))
        'CHANX 0,0->10,0'
        >>> str(Track((0, 0), (0, 10)))
        'CHANY 0,0->0,10'
        >>> str(Track((1, 2), (3, 2), idx=5))
        'CHANX 1,2->3,2 @5'
        >>> str(Track((1, 2), (3, 2), name="ABC"))
        'ABC'
        >>> str(Track((1, 2), (3, 2), idx=5, name="ABC"))
        'ABC@5'
        """
        idx_str = ""
        if self.idx is not None:
            idx_str = " @{}".format(self.idx)
        if self.name:
            return "{}{}".format(self.name, idx_str[1:])
        return "{} {},{}->{},{}{}".format(
            self.type.value, self.start.x, self.start.y, self.end.x,
            self.end.y, idx_str
        )


# Short alias.
T = Track


class ChannelGrid(dict):
    """
    Functionality:
     * Manages single type of track (either `CHANX` or `CHANY`).
     * Manages channel width along grid.
     * Allocates tracks within channels.

    The `ChannelGrid` is indexed by `Position` and returns a sequence width all
    the `Track`s at that position.
    """

    def __init__(self, size, chan_type):
        """
        size: Size representing tile grid width/height
        chan_type: of Channels.Type
        """
        self.chan_type = chan_type
        self.size = Size(*size)

        self.clear()

    @property
    def width(self):
        """Grid width

        >>> g = ChannelGrid((6, 7), Track.Type.Y)
        >>> g.width
        6
        """
        return self.size.width

    @property
    def height(self):
        """Grid height

        >>> g = ChannelGrid((6, 7), Track.Type.Y)
        >>> g.height
        7
        """
        return self.size.height

    def column(self, x):
        """Get a y coordinate indexed list giving tracks at that x + y position"""
        column = []
        for y in range(0, self.height):
            column.append(self[Position(x, y)])
        return column

    def row(self, y):
        """Get an x coordinate indexed list giving tracks at that x + y position"""
        row = []
        for x in range(0, self.width):
            row.append(self[Position(x, y)])
        return row

    """
    dim_*: CHANX/CHANY abstraction functions
    These can be used to write code that is not aware of specifics related to CHANX vs CHANY
    """

    def dim_rc(self):
        """Get dimension a, the number of row/col positions"""
        return {
            Track.Type.X: self.height,
            Track.Type.Y: self.width,
        }[self.chan_type]

    def dim_chanl(self):
        """Get dimension b, the number of valid track positions within a specific channel"""
        return {
            Track.Type.X: self.width,
            Track.Type.Y: self.height,
        }[self.chan_type]

    def foreach_position(self):
        """Generate all valid placement positions (exclude border)"""
        xmin, ymin = {
            Track.Type.X: (1, 0),
            Track.Type.Y: (0, 1),
        }[self.chan_type]

        for row in range(ymin, self.height):
            for col in range(xmin, self.width):
                yield Position(col, row)

    def foreach_track(self):
        """Generate all current legal channel positions (exclude border)"""
        for pos in self.foreach_position():
            for ti, t in enumerate(self[pos]):
                yield (pos, ti, t)

    def slicen(self):
        """Get grid width or height corresponding to chanx/chany type"""
        return {
            Track.Type.X: self.height,
            Track.Type.Y: self.width,
        }[self.chan_type]

    def slice(self, i):
        """Get row or col corresponding to chanx/chany type"""
        return {
            Track.Type.X: self.row,
            Track.Type.Y: self.column,
        }[self.chan_type](
            i
        )

    def track_slice(self, t):
        """Get the row or column the track runs along"""
        return {
            Track.Type.X: self.row,
            Track.Type.Y: self.column,
        }[t.type](
            t.common
        )

    def tracks(self):
        """Get all channels in a set"""
        ret = set()
        for _pos, _ti, t in self.foreach_track():
            ret.add(t)
        return ret

    def validate_pos(self, pos, msg=''):
        """
        A channel must go between switchboxes (where channels can cross)
        Channels are upper right of tile
        Therefore, the first position in a channel cannot have a track because
        there is no proceeding switchbox"""
        if msg:
            msg = msg + ': '
        # Gross error out of grid
        if pos.x < 0 or pos.y < 0 or pos.x >= self.width or pos.y >= self.height:
            raise ValueError(
                "%sGrid %s, point %s out of grid size coordinate" %
                (msg, self.size, pos)
            )

        if self.chan_type == Track.Type.X and pos.x == 0:
            raise ValueError("%sInvalid CHANX x=0 point %s" % (msg, pos))
        if self.chan_type == Track.Type.Y and pos.y == 0:
            raise ValueError("%sInvalid CHANY y=0 point %s" % (msg, pos))

    def create_track(self, t, idx=None):
        """
        Channel allocator
        Finds an optimal place to put the channel, increasing the channel width if necessary
        If idx is given, it must go there
        Throw exception if location is already occupied

        >>> g = ChannelGrid((11, 11), Track.Type.X)
        >>> # Adding the first channel
        >>> g.create_track(Track((1, 6), (4, 6), name="A"))
        T(A,0)
        >>> g[(1,6)]
        [T(A,0)]
        >>> g[(2,6)]
        [T(A,0)]
        >>> g[(4,6)]
        [T(A,0)]
        >>> g[(5,6)]
        [None]
        >>> # Adding second non-overlapping second channel
        >>> g.create_track(Track((5, 6), (7, 6), name="B"))
        T(B,0)
        >>> g[(4,6)]
        [T(A,0)]
        >>> g[(5,6)]
        [T(B,0)]
        >>> g[(7,6)]
        [T(B,0)]
        >>> g[(8,6)]
        [None]
        >>> # Adding third channel which overlaps with second channel
        >>> g.create_track(Track((5, 6), (7, 6), name="T"))
        T(T,1)
        >>> g[(4,6)]
        [T(A,0), None]
        >>> g[(5,6)]
        [T(B,0), T(T,1)]
        >>> g[(7,6)]
        [T(B,0), T(T,1)]
        >>> # Adding a channel which overlaps, but is a row over
        >>> g.create_track(Track((5, 7), (7, 7), name="D"))
        T(D,0)
        >>> g[(5,6)]
        [T(B,0), T(T,1)]
        >>> g[(5,7)]
        [T(D,0)]
        >>> # Adding fourth channel which overlaps both the first
        >>> # and second+third channel
        >>> g.create_track(Track((3, 6), (6, 6), name="E"))
        T(E,2)
        >>> g[(2,6)]
        [T(A,0), None, None]
        >>> g[(3,6)]
        [T(A,0), None, T(E,2)]
        >>> g[(6,6)]
        [T(B,0), T(T,1), T(E,2)]
        >>> g[(7,6)]
        [T(B,0), T(T,1), None]
        >>> # This channel fits in the hole left by the last one.
        >>> g.create_track(Track((1, 6), (3, 6), name="F"))
        T(F,1)
        >>> g[(1,6)]
        [T(A,0), T(F,1), None]
        >>> g[(2,6)]
        [T(A,0), T(F,1), None]
        >>> g[(3,6)]
        [T(A,0), T(F,1), T(E,2)]
        >>> g[(4,6)]
        [T(A,0), None, T(E,2)]
        >>> # Add another channel which causes a hole
        >>> g.create_track(Track((1, 6), (7, 6), name="G"))
        T(G,3)
        >>> g[(1,6)]
        [T(A,0), T(F,1), None, T(G,3)]
        >>> g[(2,6)]
        [T(A,0), T(F,1), None, T(G,3)]
        >>> g[(3,6)]
        [T(A,0), T(F,1), T(E,2), T(G,3)]
        >>> g[(4,6)]
        [T(A,0), None, T(E,2), T(G,3)]
        >>> g[(5,6)]
        [T(B,0), T(T,1), T(E,2), T(G,3)]
        >>> g[(6,6)]
        [T(B,0), T(T,1), T(E,2), T(G,3)]
        >>> g[(7,6)]
        [T(B,0), T(T,1), None, T(G,3)]
        >>> g[(8,6)]
        [None, None, None, None]
        """
        assert t.idx is None
        force_idx = idx

        self.validate_pos(t.start, 'start')
        self.validate_pos(t.end, 'end')

        if t.type_guess != self.chan_type:
            if t.length != 0:
                raise TypeError(
                    "Can only add channels of type {} which {} ({}) is not.".
                    format(self.chan_type, t, t.type)
                )
            else:
                t.type_hint = self.chan_type

        c = self.track_slice(t)
        assert_len_eq(c)

        # Find start and end
        s, e = min(t.start0, t.end0), max(t.start0, t.end0)
        assert e >= s, (e, '>=', s)
        assert s < len(c), (s, '<', len(c), c)
        assert e < len(c), (e + 1, '<', len(c), c)

        # Find a idx that this channel fits.
        # Normally start at first channel (0) unless forcing to a specific channel
        max_idx = force_idx if force_idx is not None else 0
        while True:
            # Check each position if the track can fit
            # Expanding channel width as required index grows
            for p in c[s:e + 1]:
                while len(p) < max_idx + 1:
                    p.append(None)
                # Can't place here?
                if p[max_idx] is not None:
                    # Grow track width
                    if force_idx is not None:
                        raise IndexError(
                            "Can't fit channel at index %d" % force_idx
                        )
                    max_idx += 1
                    break
            # Was able to place into all locations
            else:
                break

        # Make sure everything has the same length.
        for p in c:
            while len(p) < max_idx + 1:
                p.append(None)
        assert_len_eq(c)

        t = t.new_idx(max_idx)
        assert t.idx == max_idx
        for p in c[s:e + 1]:
            p[t.idx] = t
        return t

    def pretty_print(self):
        """
        If type == Track.Type.X

          A--AC-C
          B-----B

          D--DE-E
          F-----F

        If type == Track.Type.Y

          AB  DF
          ||  ||
          ||  ||
          A|  D|
          C|  E|
          ||  ||
          CB  EF

        """

        def get_str(t):
            if not t:
                s = ""
            elif t.name:
                s = t.name
            else:
                s = str(t)
            return s

        # Work out how many characters the largest label takes up.
        s_maxlen = 1
        for row in range(0, self.height):
            for col in range(0, self.width):
                for t in self[(col, row)]:
                    s_maxlen = max(s_maxlen, len(get_str(t)))

        assert s_maxlen > 0, s_maxlen
        s_maxlen += 3
        if self.chan_type == Track.Type.Y:
            beg_fmt = "{:^%i}" % s_maxlen
            end_fmt = beg_fmt
            mid_fmt = beg_fmt.format("||")
        elif self.chan_type == Track.Type.X:
            beg_fmt = "{:>%i}>" % (s_maxlen - 1)
            end_fmt = "->{:<%i}" % (s_maxlen - 2)
            mid_fmt = "-" * s_maxlen
        else:
            assert False
        non_fmt = " " * s_maxlen
        """
        rows[row][col][c]
        row: global row location
        col: column of output
        c: character showing occupation along a track

        Channel width may vary across tiles, but all columns within that region
        should have the same length
        """
        rows = []
        for y in range(0, self.height):
            cols = []
            for x in range(0, self.width):
                # Header
                hdri = {Track.Type.X: x, Track.Type.Y: y}[self.chan_type]
                channels = [("|{: ^%i}" % (s_maxlen - 1)).format(hdri)]

                for t in self[(x, y)]:
                    if not t:
                        fmt = non_fmt
                    elif t.start == t.end:
                        s = get_str(t)
                        channels.append(
                            "{} ".format(
                                "".join(
                                    [
                                        beg_fmt.format(s),
                                        mid_fmt.format(s),
                                        end_fmt.format(s),
                                    ]
                                )[:s_maxlen - 1]
                            )
                        )
                        continue
                    elif t.start == (x, y):
                        fmt = beg_fmt
                    elif t.end == (x, y):
                        fmt = end_fmt
                    else:
                        fmt = mid_fmt

                    channels.append(fmt.format(get_str(t)))
                cols.append(channels)
            rows.append(cols)

        # Dump the track state as a string
        f = io.StringIO()

        def p(*args, **kw):
            print(*args, file=f, **kw)

        if self.chan_type == Track.Type.X:
            for r in range(0, len(rows)):
                assert_len_eq(rows[r])
                # tracks + 1 for header
                track_rows = len(rows[r][0])
                for tracki in range(0, track_rows):
                    for c in range(0, len(rows[r])):
                        p(rows[r][c][tracki], end="")
                    # Close header
                    if tracki == 0:
                        p("|", end="")
                    p()
                p("\n")
        elif self.chan_type == Track.Type.Y:
            for r in range(0, len(rows)):
                # tracks + 1 for header
                for c in range(0, len(rows[r])):
                    track_cols = len(rows[r][c])
                    p("|*", end="")
                    for tracki in range(0, track_cols):
                        p(rows[r][c][tracki], end="")
                        # Close header
                        if tracki == 0:
                            p("|", end="")
                    p("  ", end="")
                p("")
                # p("|*|")
        else:
            assert False

        return f.getvalue()

    def clear(self):
        """Remove tracks from all currently occupied positions, making channel width 0"""
        for x in range(0, self.width):
            for y in range(0, self.height):
                self[Position(x, y)] = []

    def check(self):
        """Self integrity check"""
        # Verify uniform track length
        if self.chan_type == Track.Type.X:
            for y in range(self.height):
                assert_len_eq(self.row(y))
        elif self.chan_type == Track.Type.Y:
            for x in range(self.width):
                assert_len_eq(self.column(x))
        else:
            assert False

    def density(self):
        """Return (number occupied positions, total number positions)"""
        occupied = 0
        net = 0
        for _pos, _ti, t in self.foreach_track():
            net += 1
            if t is not None:
                occupied += 1
        return occupied, net

    def fill_empty(self, segment_id, name=None):
        tracks = []
        for pos, ti, t in self.foreach_track():
            if t is None:
                tracks.append(
                    self.create_track(
                        Track(
                            pos,
                            pos,
                            segment_id=segment_id,
                            name=name,
                            type_hint=self.chan_type,
                            direction=Track.Direction.BI
                        ),
                        idx=ti
                    )
                )
        return tracks

    def channel_widths(self):
        """Return (min channel width, max channel width, row/col widths)"""
        cwmin = float('+inf')
        cwmax = float('-inf')
        xy_list = []
        for i in range(self.slicen()):
            # track width should be consistent along a slice
            # just take the first element
            loc = self.slice(i)[0]
            cwmin = min(cwmin, len(loc))
            cwmax = max(cwmax, len(loc))
            xy_list.append(len(loc))
        return (cwmin, cwmax, xy_list)

    def assert_width(self, width):
        """Assert all channels have specified --route_chan_width"""
        for pos in self.foreach_position():
            tracks = self[pos]
            assert len(
                tracks
            ) == width, 'Bad width Position(x=%d, y=%d): expect %d, got %d' % (
                pos.x, pos.y, width, len(tracks)
            )

    def assert_full(self):
        """Assert all allocated channels are fully occupied"""
        self.check()
        # occupied, net = self.density()
        # print("Occupied %d / %d" % (occupied, net))
        for pos, ti, t in self.foreach_track():
            assert t is not None, 'Unoccupied Position(x=%d, y=%d) track=%d' % (
                pos.x, pos.y, ti
            )


class Channels:
    """Holds all channels for the whole grid (X + Y)"""

    def __init__(self, size):
        self.size = size
        self.x = ChannelGrid(size, Track.Type.X)
        self.y = ChannelGrid(size, Track.Type.Y)

    def create_diag_track(self, start, end, segment_id, idx=None):
        # Actually these can be tuple as well
        # assert_type(start, Pos)
        # assert_type(end, Pos)

        # Create track(s)
        try:
            return (self.create_xy_track(start, end, segment_id, idx=idx), )
        except ChannelNotStraight:
            assert idx is None, idx
            corner = (start.x, end.y)
            ta = self.create_xy_track(start, corner, segment_id)[0]
            tb = self.create_xy_track(corner, end, segment_id)[0]
            return (ta, tb)

    def create_xy_track(
            self,
            start,
            end,
            segment_id,
            idx=None,
            name=None,
            typeh=None,
            direction=None
    ):
        """
        idx: None to automatically allocate
        """
        # Actually these can be tuple as well
        # assert_type(start, Pos)
        # assert_type(end, Pos)

        # Create track(s)
        # Will throw exception if not straight
        t = Track(
            start,
            end,
            segment_id=segment_id,
            name=name,
            type_hint=typeh,
            direction=direction
        )

        # Add the track to associated channel list
        # Get the track now with the index assigned
        t = {
            Track.Type.X: self.x.create_track,
            Track.Type.Y: self.y.create_track
        }[t.type](
            t, idx=idx
        )
        # print('create %s %s to %s idx %s' % (t.type, start, end, idx))

        assert t.idx is not None
        if typeh:
            assert t.type == typeh, (t.type.value, typeh)
        return t

    def pad_channels(self, segment_id):
        tracks = []
        tracks.extend(self.x.fill_empty(segment_id))
        tracks.extend(self.y.fill_empty(segment_id))
        return tracks

    def pretty_print(self):
        s = ''
        s += 'X\n'
        s += self.x.pretty_print()
        s += 'Y\n'
        s += self.y.pretty_print()
        return s

    def clear(self):
        """Remove all channels"""
        self.x.clear()
        self.y.clear()

    def from_xml_nodes(self, nodes_xml):
        """Add channels from <nodes> CHANX/CHANY"""
        for node_xml in nodes_xml:
            ntype = node_xml.get('type')
            if ntype not in ('CHANX', 'CHANY'):
                continue
            ntype_e = Track.Type(ntype)

            direction = Track.Direction(node_xml.get('direction'))

            loc = single_element(node_xml, 'loc')
            idx = int(loc.get('ptc'))
            pos_low, pos_high = node_pos(node_xml)
            # print('Importing %s @ %s:%s :: %d' % (ntype, pos_low, pos_high, idx))

            segment_xml = single_element(node_xml, 'segment')
            segment_id = int(segment_xml.get('segment_id'))

            # idx will get assigned when adding to track
            try:
                self.create_xy_track(
                    pos_low,
                    pos_high,
                    segment_id,
                    idx=idx,
                    # XML has no name concept. Should it?
                    name=None,
                    typeh=ntype_e,
                    direction=direction
                )
            except Exception:
                print("Bad XML: %s" % (ET.tostring(node_xml)))
                raise

    def to_xml_channels(self, channels_xml):
        channels_xml.clear()

        # channel entry
        cw_xmin, cw_xmax, x_lists = self.x.channel_widths()
        cw_ymin, cw_ymax, y_lists = self.y.channel_widths()
        cw_max = max(cw_xmax, cw_ymax)
        ET.SubElement(
            channels_xml, 'channel', {
                'chan_width_max': str(cw_max),
                'x_min': str(cw_xmin),
                'x_max': str(cw_xmax),
                'y_min': str(cw_ymin),
                'y_max': str(cw_ymax),
            }
        )
        # x_list / y_list tries
        for i, info in enumerate(x_lists):
            ET.SubElement(
                channels_xml, 'x_list', {
                    'index': str(i),
                    'info': str(info)
                }
            )
        for i, info in enumerate(y_lists):
            ET.SubElement(
                channels_xml, 'y_list', {
                    'index': str(i),
                    'info': str(info)
                }
            )

    def to_xml(self, xml_graph):
        self.to_xml_channels(single_element(xml_graph, 'channels'))


def TX(start, end, idx=None, name=None, direction=None, segment_id=None):
    if direction is None:
        direction = Track.Direction.INC
    if segment_id is None:
        segment_id = 0
    return T(
        start,
        end,
        direction=direction,
        segment_id=segment_id,
        idx=idx,
        name=name,
        type_hint=Track.Type.X,
    )


def TY(start, end, idx=None, name=None, direction=None, segment_id=None):
    if direction is None:
        direction = Track.Direction.INC
    if segment_id is None:
        segment_id = 0
    return T(
        start,
        end,
        direction=direction,
        segment_id=segment_id,
        idx=idx,
        name=name,
        type_hint=Track.Type.Y,
    )


def docprint(x):
    pprint.pprint(x.splitlines())


def create_test_channel_grid():
    g = ChannelGrid((6, 3), Track.Type.X)
    g.create_track(TX((1, 0), (5, 0), name="AA"))
    g.create_track(TX((1, 0), (3, 0), name="BB"))
    g.create_track(TX((2, 0), (5, 0), name="CC"))
    g.create_track(TX((1, 0), (1, 0), name="DD"))

    g.create_track(TX((1, 1), (3, 1), name="aa"))
    g.create_track(TX((4, 1), (5, 1), name="bb"))
    g.create_track(TX((1, 1), (5, 1), name="cc"))

    g.check()
    return g


def test_x_auto():
    """
    >>> docprint(test_x_auto())
    ['| 0  | 1  | 2  | 3  | 4  | 5  |',
     '       AA>---------------->AA ',
     '       BB>------>BB           ',
     '       DD   CC>----------->CC ',
     '',
     '',
     '| 0  | 1  | 2  | 3  | 4  | 5  |',
     '       aa>------>aa   bb>->bb ',
     '       cc>---------------->cc ',
     '',
     '',
     '| 0  | 1  | 2  | 3  | 4  | 5  |',
     '',
     '']
    """
    g = create_test_channel_grid()
    return g.pretty_print()


def test_pad():
    """
    >>> docprint(test_pad())
    ['| 0  | 1  | 2  | 3  | 4  | 5  |',
     '       AA>---------------->AA ',
     '       BB>------>BB   XX   XX ',
     '       DD   CC>----------->CC ',
     '',
     '',
     '| 0  | 1  | 2  | 3  | 4  | 5  |',
     '       aa>------>aa   bb>->bb ',
     '       cc>---------------->cc ',
     '',
     '',
     '| 0  | 1  | 2  | 3  | 4  | 5  |',
     '',
     '']
    """
    g = create_test_channel_grid()
    g.fill_empty(0, name='XX')
    g.check()
    return g.pretty_print()


def test_x_manual():
    """
    >>> pprint.pprint(test_x_manual().splitlines())
    ['| 0  | 1  | 2  | 3  | 4  | 5  |',
     '       AA>---------------->AA ',
     '       BB>------>BB           ',
     '       DD   CC>----------->CC ',
     '',
     '',
     '| 0  | 1  | 2  | 3  | 4  | 5  |',
     '       aa>------>aa   bb>->bb ',
     '       cc>---------------->cc ',
     '',
     '',
     '| 0  | 1  | 2  | 3  | 4  | 5  |',
     '',
     '']
    """
    g = ChannelGrid((6, 3), Track.Type.X)
    g.create_track(TX((1, 0), (5, 0), name="AA"), idx=0)
    g.create_track(TX((1, 0), (3, 0), name="BB"), idx=1)
    g.create_track(TX((2, 0), (5, 0), name="CC"), idx=2)
    g.create_track(TX((1, 0), (1, 0), name="DD"), idx=2)

    g.create_track(TX((1, 1), (3, 1), name="aa"), idx=0)
    g.create_track(TX((4, 1), (5, 1), name="bb"), idx=0)
    g.create_track(TX((1, 1), (5, 1), name="cc"), idx=1)

    try:
        g.create_track(TX((1, 1), (5, 1), name="dd"), idx=1)
        assert False, "Should have failed to place"
    except IndexError:
        pass

    g.check()
    return g.pretty_print()


def test_y_auto():
    """
    >>> docprint(test_y_auto())
    ['|*| 0  |                 |*| 0  |            |*| 0  |  ',
     '|*| 1  | AA   BB   DD    |*| 1  | aa   cc    |*| 1  |  ',
     '|*| 2  | ||   ||   CC    |*| 2  | ||   ||    |*| 2  |  ',
     '|*| 3  | ||   BB   ||    |*| 3  | aa   ||    |*| 3  |  ',
     '|*| 4  | ||        ||    |*| 4  | bb   ||    |*| 4  |  ',
     '|*| 5  | AA        CC    |*| 5  | bb   cc    |*| 5  |  ']
    """
    g = ChannelGrid((3, 6), Track.Type.Y)
    g.create_track(TY((0, 1), (0, 5), name="AA"))
    g.create_track(TY((0, 1), (0, 3), name="BB"))
    g.create_track(TY((0, 2), (0, 5), name="CC"))
    g.create_track(TY((0, 1), (0, 1), name="DD"))

    g.create_track(TY((1, 1), (1, 3), name="aa"))
    g.create_track(TY((1, 4), (1, 5), name="bb"))
    g.create_track(TY((1, 1), (1, 5), name="cc"))

    g.check()
    return g.pretty_print()


if __name__ == "__main__":
    import doctest
    print('doctest: begin')
    failure_count, test_count = doctest.testmod()
    assert test_count > 0
    assert failure_count == 0, "Doctests failed!"
    print('doctest: end')
