"""
Double decker train problem:

               |^^^^^^^^^^^^^^|   |^^^^^^^^^^^^^^|
               | Double cargo |   |       wagon  |
   ~~~~ ____   |--------------|   |--------------|
  Y_,___|[]|   | Double       |   | cargo wagon  |
 {|_|_|_|PU|_,_|______________|_,_|______________|
//oo---OO=OO     O-O      O-O ^     O-O      O-O ^

This is a deceptively simple programming puzzle where you get to make an ordering of rail wagons.
Each wagon has two floors and there is text on the outside of the top and bottom floor.
When your chosen wagons are linked together, the whole text on the top should match the whole bottom.
Output an empty list only when there is no non-empty choice of wagons with matching text.
Your ordering may include any wagon any number of times.

Example: (illustrated above)
    Input wagons: [("Double cargo ", "Double "), ("wagon", "cargo wagon")]
    Output ordering: [0, 1]

    Explanation:
    Both the top and bottom will read "Double cargo wagon" when concatenated.
    Note that there is no need to mutate whitespace characters in the input.
"""

from typing import Callable, List, Optional, Tuple

Wagon = Tuple[str, str]


MAX_LENGTH = 10

def my_solver(wagons: List[Wagon]) -> List[int]:
    # Handle empty wagon case
    for i, wagon in enumerate(wagons):
        if wagon[0] + wagon[1] == "":
            return [i]
    return solve("", "", [], prepare(wagons))

def prepare(wagons: List[Wagon]) -> List[Tuple[int, Wagon]]:
    seen = set()
    prepared = []
    for i, wagon in enumerate(wagons):
        if wagon in seen:
            continue
        prepared.append((i, wagon))
        seen.add(wagon)
    return prepared

def solve(top_text: str, bottom_text: str, ordering: List[int], wagons: List[Tuple[int, Wagon]]) -> List[int]:
    if len(top_text) == 0 and len(bottom_text) == 0 and len(ordering) > 0:
        return ordering
    if len(ordering) > MAX_LENGTH:
        return []

    for z, (i, wagon) in enumerate(wagons):
        top = top_text + wagon[0]
        bottom = bottom_text + wagon[1]
        reordered = wagons[:z] + wagons[z+1:] + [(i, wagon)]
        if top.startswith(bottom):
            solution = solve(top[len(bottom):], "", [*ordering, i], reordered)
            if len(solution) > 0:
                return solution
        if bottom.startswith(top):
            solution = solve("", bottom[len(top):], [*ordering, i], reordered)
            if len(solution) > 0:
                return solution

    return []

def check_ordering(wagons: List[Wagon], ordering: List[int]) -> bool:
    """Checks if the text on top matches the text on the bottom"""
    sentence0 = ""
    sentence1 = ""
    for i in ordering:
        assert 0 <= i < len(wagons), f"wagon index out of range: {i}"
        sentence0 += wagons[i][0]
        sentence1 += wagons[i][1]
    return sentence0 == sentence1


def run_test_case(
    wagons: List[Wagon],
    example_ordering: List[int],
    ordering: List[int],
) -> str:
    if ordering == []:
        if example_ordering == []:
            return "PASS"
        return "You falsely claimed it was impossible."

    if check_ordering(wagons, ordering):
        if example_ordering == []:
            return "PASS (and the problem setter is a dummy)"
        return "PASS"

    sentence0 = ""
    sentence1 = ""
    for i in ordering:
        assert 0 <= i < len(wagons), f"wagon index out of range: {i}"
        sentence0 += wagons[i][0]
        sentence1 += wagons[i][1]

    # return f"Your selected wagons had different sentences on the top and bottom.\n{ordering}\n{sentence0!r}\n{sentence1!r}"

    return "Your selected wagons had different sentences on the top and bottom."


def run_test_cases(solver: Callable[[List[Wagon]], List[int]]):
    test_cases: Tuple[List[Wagon], List[int]] = [
        ([("Double cargo ", "Double "), ("wagon", "cargo wagon")], [0, 1]),
        ([("wagon", "cargo wagon"), ("Double cargo ", "Double ")], [1, 0]),
        ([("Hello ", "Hello"), ("World", " World")], [0, 1]),
        ([("", "")], [0]),
        ([(".", "-.."), (".-", ".."), ("--.", "--")], [2, 1, 2, 0]),
        ([("bc", "ca"), ("a", "ab"), ("ca", "a"), ("abc", "c")], [1, 0, 1, 3]),
        ([("yy", "y"), ("xy", "yx"), ("z", "yz")], [0, 1, 1, 1, 1, 1, 2]),
        ([("", "42"), ("42", "")], [1, 0]),
        ([("aaaa", ""), ("", "a")], [0, 1, 1, 1, 1]),
        ([], []),
        ([("0", "1")], []),
        ([("0", "1"), (" ", "  ")], []),
        ([("", " ")], []),
        ([("aa", "a"), ("aa", "a"), ("b", "ab")], [0, 2]),
        ([("aa", ""), ("ab", ""), ("", "aa")], [0, 2]),
        ([("aa", ""), ("ab", "")], []),
        ([("ab", "a"), ("ab", "b")], []),
        ([("aaaa", ""), ("", "a")], [0, 1, 1, 1, 1]),
        ([("ab"*1000, ""), ("a", "ab"), ("b", "ab")], [0] + [1,2]*1000),
        ([("a"*(2**4*3**5), ""), ("", "aa"), ("", "aaa")], [0]+[1]*2**4 +[2]*3**5),

        # ([("ab", "a"), ("ab", "b")], []),
        # TODO: add more test cases
    ]
    for i, (wagons, example_ordering) in enumerate(test_cases):
        print(f"Test case {i:>2}: ", end="")
        if not check_ordering(wagons, example_ordering):
            sentence0 = ""
            sentence1 = ""
            for i in example_ordering:
                assert 0 <= i < len(wagons), f"wagon index out of range: {i}"
                sentence0 += wagons[i][0]
                sentence1 += wagons[i][1]
            print(sentence0)
            print(sentence1)


        assert check_ordering(wagons, example_ordering), "example is invalid, dummy."

        print(run_test_case(wagons, example_ordering, solver(wagons)))


run_test_cases(my_solver)
