package mastermind;

import java.awt.Color;
import java.util.Random;
import javalib.funworld.*;
import javalib.worldimages.*;
import tester.Tester;

class Examples {

    ILoInt noInt = new MtInt();
    ILoInt intsOne = new ConsInt(1, new ConsInt(2, new ConsInt(3, noInt)));

    Dot redDot = new Dot(Color.red);
    Dot greenDot = new Dot(Color.green);
    Dot blueDot = new Dot(Color.blue);
    Dot reenDot = new Dot(new Color(255, 255, 0));

    ILoDot noDot = new MtDot();
    ILoDot dotsOne = new ConsDot(
        redDot,
        new ConsDot(greenDot, new ConsDot(blueDot, noDot)),
        new Random(0)
    );
    ILoDot dotsTwo = new ConsDot(
        redDot,
        new ConsDot(blueDot, new ConsDot(blueDot, noDot))
    );
    ILoDot dotsThree = new ConsDot(
        reenDot,
        new ConsDot(reenDot, new ConsDot(reenDot, noDot))
    );
    ILoDot exampleDotsOne = new ConsDot(
        redDot,
        new ConsDot(
            greenDot,
            new ConsDot(
                blueDot,
                new ConsDot(
                    greenDot,
                    new ConsDot(redDot, new ConsDot(greenDot, noDot))
                )
            )
        )
    );
    ILoDot exampleDotsTwo = new ConsDot(
        redDot,
        new ConsDot(
            blueDot,
            new ConsDot(
                greenDot,
                new ConsDot(
                    greenDot,
                    new ConsDot(blueDot, new ConsDot(blueDot, noDot))
                )
            )
        )
    );
    ILoDot exampleDotsOneNoExact = new ConsDot(
        greenDot,
        new ConsDot(blueDot, new ConsDot(redDot, new ConsDot(greenDot, noDot)))
    );
    ILoDot exampleDotsTwoNoExact = new ConsDot(
        blueDot,
        new ConsDot(greenDot, new ConsDot(blueDot, new ConsDot(blueDot, noDot)))
    );

    Feedback exampleFeedback = new Feedback(2, 2);

    Game exampleGame = new Game(
        new GameConf(
            true,
            6,
            4,
            new ConsDot(
                redDot,
                new ConsDot(greenDot, new ConsDot(blueDot, new MtDot()))
            )
        ),
        new ConsDot(
            redDot,
            new ConsDot(
                greenDot,
                new ConsDot(
                    blueDot,
                    new ConsDot(
                        greenDot,
                        new ConsDot(redDot, new ConsDot(greenDot, new MtDot()))
                    )
                )
            )
        ),
        4,
        ConsPlaceholderGuess.mkN(4),
        false,
        false
    );

    boolean testDrawMethods(Tester t) {
        // WorldImage incomplete = new IncompleteGuess(exampleDotsOne).draw();
        // WorldImage guesses = new ConsGuess(new Guess(dotsOne, new Feedback(1, 2)),
        // new ConsGuess(new Guess(dotsTwo, new Feedback(2, 5)), new MtGuess())).draw();
        //Game game = new Game().addGuess(new Guess(dotsOne, exampleFeedback));

        /*
        WorldImage gameImg = game.draw();

        int w = (int) gameImg.getWidth();
        int h = (int) gameImg.getHeight();

        WorldCanvas c = new WorldCanvas(w, h);
        WorldScene s = new WorldScene(w, h);
        */

        return exampleGame.launch();
        // c.drawScene(s.placeImageXY(gameImg, w/2, h/2))
        // c.drawScene(s.placeImageXY(incomplete, (int) (incomplete.getWidth()/2), 100))
        // c.drawScene(s.placeImageXY(exampleFeedback.draw(), 100, 100))
        // c.drawScene(s.placeImageXY(redDot.draw(), 100, 100))
        // c.drawScene(s.placeImageXY(dotsOne.draw(), 250, 250))
        // && c.show();
        // true;
    }

    boolean testILoDotLen(Tester t) {
        return t.checkExpect(noDot.len(), 0) && t.checkExpect(dotsOne.len(), 3);
    }

    boolean testILoDotGetnth(Tester t) {
        return (
            t.checkException(
                new IllegalArgumentException("Index out of bounds."),
                noDot,
                "get",
                1
            ) &&
            t.checkException(
                new IllegalArgumentException("Index out of bounds."),
                dotsOne,
                "get",
                3
            ) &&
            t.checkExpect(dotsOne.get(0), redDot) &&
            t.checkExpect(dotsOne.get(2), blueDot)
        );
    }

    boolean testILoDotGen(Tester t) {
        return (
            t.checkExpect(noDot.gen(219), noDot) &&
            t.checkExpect(dotsOne.gen(1), new ConsDot(redDot, noDot)) &&
            t.checkExpect(
                dotsOne.gen(3),
                new ConsDot(
                    greenDot,
                    new ConsDot(greenDot, new ConsDot(blueDot, noDot))
                )
            )
        );
    }

    boolean testILoDotIn(Tester t) {
        return (
            t.checkExpect(dotsOne.in(blueDot), true) &&
            t.checkExpect(noDot.in(redDot), false) &&
            t.checkExpect(dotsThree.in(blueDot), false)
        );
    }

    boolean testILoIntRemove(Tester t) {
        return (
            t.checkExpect(
                intsOne.remove(0),
                new ConsInt(2, new ConsInt(3, noInt))
            ) &&
            t.checkExpect(
                intsOne.remove(2),
                new ConsInt(1, new ConsInt(2, noInt))
            ) &&
            t.checkException(
                new IllegalArgumentException(
                    "Index out of bounds: cannot remove something from nothing."
                ),
                intsOne,
                "remove",
                3
            ) &&
            t.checkException(
                new IllegalArgumentException(
                    "Index out of bounds: cannot remove something from nothing."
                ),
                noInt,
                "remove",
                0
            ) &&
            t.checkException(
                new IllegalArgumentException("Indices must be positive."),
                intsOne,
                "remove",
                -2
            )
        );
    }

    boolean testILoIntLen(Tester t) {
        return t.checkExpect(intsOne.len(), 3) && t.checkExpect(noInt.len(), 0);
    }

    boolean testILoIntSubOne(Tester t) {
        return t.checkExpect(
            intsOne.subOne(),
            new ConsInt(0, new ConsInt(1, new ConsInt(2, noInt)))
        );
    }

    boolean testILoDotExactIndices(Tester t) {
        return (
            t.checkException(
                new IllegalArgumentException(
                    "Empty list doesn't exactly match anything."
                ),
                noDot,
                "exactIndices",
                noDot
            ) &&
            t.checkExpect(
                dotsOne.exactIndices(dotsOne),
                new ConsInt(0, new ConsInt(1, new ConsInt(2, noInt)))
            ) &&
            t.checkExpect(
                dotsOne.exactIndices(dotsTwo),
                new ConsInt(0, new ConsInt(2, new MtInt()))
            )
        );
    }

    boolean testILoDotRemove(Tester t) {
        return (
            t.checkException(
                new IllegalArgumentException("Index out of bounds."),
                noDot,
                "remove",
                0
            ) &&
            t.checkExpect(
                dotsOne.remove(0),
                new ConsDot(greenDot, new ConsDot(blueDot, noDot))
            ) &&
            t.checkExpect(
                dotsTwo.remove(1),
                new ConsDot(redDot, new ConsDot(blueDot, noDot))
            )
        );
    }

    boolean testILoDotRemoveAll(Tester t) {
        return (
            t.checkExpect(dotsOne.removeAll(noInt), dotsOne) &&
            t.checkExpect(
                dotsOne.removeAll(
                    new ConsInt(0, new ConsInt(1, new ConsInt(2, noInt)))
                ),
                noDot
            ) &&
            t.checkExpect(noDot.removeAll(intsOne), noDot) &&
            t.checkExpect(
                dotsTwo.removeAll(new ConsInt(1, new ConsInt(2, noInt))),
                new ConsDot(redDot, noDot)
            )
        );
    }

    boolean testILoDotCountInexact(Tester t) {
        return (
            t.checkExpect(noDot.countInexact(noDot), 0) &&
            t.checkExpect(dotsOne.countInexact(dotsOne), 3) &&
            t.checkExpect(
                exampleDotsOneNoExact.countInexact(exampleDotsTwoNoExact),
                2
            )
        );
    }

    boolean testILoDotCompare(Tester t) {
        return (
            t.checkExpect(
                exampleDotsOne.compare(exampleDotsTwo),
                exampleFeedback
            ) &&
            t.checkExpect(dotsOne.compare(dotsOne), new Feedback(3, 0))
        );
    }
}

class Util {

    static int scale = 2;
    static int fontSz = 24 * scale;
    static int gapW = 4 * scale; // The gap between objects.

    // Drawing methods.
    static WorldImage gap = new RectangleImage(
        gapW,
        gapW,
        "outline",
        new Color(0, 0, 0, 0)
    );

    static WorldImage pairGap(WorldImage img1, WorldImage img2) {
        return new BesideAlignImage(AlignModeY.MIDDLE, img1, gap, img2);
    }

    static WorldImage pairGapAbove(WorldImage img1, WorldImage img2) {
        return new AboveAlignImage(AlignModeX.CENTER, img1, gap, img2);
    }

    static WorldImage strPair(String str1, String str2) {
        return pairGap(
            new TextImage(str1, fontSz, Color.black),
            new TextImage(str2, fontSz, Color.black)
        );
    }

    static int pairgapW = 2 * 16 * scale + gapW;
}

// A game state.
class Game extends World {

    static ILoDot DEFAULTDOTS = new ConsDot(
        new Dot(Color.RED),
        new ConsDot(
            new Dot(Color.GREEN),
            new ConsDot(new Dot(Color.BLUE), new MtDot())
        )
    );
    static GameConf DEFAULTCONF = new GameConf(true, 5, 5, DEFAULTDOTS);

    GameConf conf; // The game's configuration.
    ILoDot solution; // The solution to the game.
    int guessesLeft; // The number of guesses the player has left.
    ILoGuess guesses; // The guesses the player has taken.
    boolean done; // Is the game over?
    boolean won; // Did they player win?

    Game(
        GameConf conf,
        ILoDot solution,
        int guessesLeft,
        ILoGuess guesses,
        boolean won,
        boolean done
    ) {
        if (this.won && !this.done) throw new IllegalArgumentException(
            "Can't win before you've finished playing."
        );
        this.conf = conf;
        this.solution = solution;
        this.won = won;
        this.done = done;
        this.guessesLeft = guessesLeft;
        this.guesses = guesses;
    }

    // Convenience constructor using default config and starting values.
    Game() {
        this(
            DEFAULTCONF,
            DEFAULTCONF.options.gen(DEFAULTCONF.len),
            DEFAULTCONF.nguesses,
            ConsPlaceholderGuess.mkN(DEFAULTCONF.nguesses),
            false,
            false
        );
    }

    public WorldScene makeScene() {
        WorldImage img = this.draw();
        int w = (int) img.getWidth(), h = (int) img.getHeight();
        WorldScene bg = new WorldScene(w, h);
        return bg.placeImageXY(img, (int) w / 2, (int) h / 2);
    }

    boolean launch() {
        return this.bigBang(this.calcW(), this.calcH());
    }

    public Game onKeyEvent(String key) {
        if ("123456789".contains(key)) { // User has entered a number.
            int choice = Integer.valueOf(key);
            if (choice <= this.conf.options.len()) return this.addDot(
                    choice - 1
                );
        } else if (key.equals("backspace")) return this.dropDot();
        else if (key.equals("enter")) return this.commitGuess();
        return this;
    }

    // Attempt to add the nth dot option to the incomplete guess.
    Game addDot(int choice) {
        return new Game(
            this.conf,
            this.solution,
            this.guessesLeft,
            this.guesses.addToIncomplete(
                    this.conf.options.get(choice),
                    this.conf.len
                ),
            this.won,
            this.done
        );
    }

    // Attempt to remove the last dot in the incomplete guess.
    Game dropDot() {
        return new Game(
            this.conf,
            this.solution,
            this.guessesLeft,
            this.guesses.dropFromIncomplete(),
            this.won,
            this.done
        );
    }

    Game commitGuess() {
        return new Game(
            this.conf,
            this.solution,
            this.guessesLeft - 1,
            this.guesses.commitIncomplete(this.conf.len, this.solution),
            this.won,
            this.done
        );
    }

    // Calculate the width of the window without drawing anything.
    int calcW() {
        return (
            (this.conf.len * Dot.r * 2) + // The width of all the dots.
            ((this.conf.len + 1) * Util.gapW) + // The width of all the spaces between the dots.
            Util.fontSz * 2 + // The width of the feedback numbers.
            Util.gapW // The width of the gap between the feedback numbers.
        );
    }

    // Calculate the height of the window without drawing anything.
    int calcH() {
        return (
            ((this.conf.nguesses + 2) * Dot.r * 2) +
            ((this.conf.nguesses) * (Util.gapW + 2))
        );
    }

    // Draw the current game state.
    WorldImage draw() {
        return new AboveAlignImage(
            AlignModeX.LEFT,
            this.draw_sol(),
            this.draw_guesses(),
            this.draw_options()
        );
    }

    // Draw the solution.
    WorldImage draw_sol() {
        /*if (this.done)*/return this.draw_sol_rev();
        /*else return this.draw_sol_hid();*/
    }

    // Draw the revealed answer.
    WorldImage draw_sol_rev() {
        return this.solution.draw();
    }

    // Draw the hidden answer.
    WorldImage draw_sol_hid() {
        return new RectangleImage(
            this.solution.getW(),
            2 * Dot.r,
            OutlineMode.OUTLINE,
            Color.BLACK
        );
    }

    WorldImage draw_guesses() {
        return this.guesses.draw();
    }

    WorldImage draw_options() {
        return this.conf.options.draw();
    }
}

// A game configuration.
class GameConf {

    boolean dups; // Whether duplicates are allowed.
    int len; // The length of the sequence to be guessed.
    int nguesses; // Number of guesses the player is allowed.
    ILoDot options; // The dots of which the solution is comprised.

    GameConf(boolean dups, int len, int nguesses, ILoDot options) {
        if (len <= 0) throw new IllegalArgumentException(
            "Length of the solution must be greater than 0."
        );
        if (nguesses <= 0) throw new IllegalArgumentException(
            "Must provide the player some guesses."
        );
        int oplen = options.len();
        if (oplen <= 0) throw new IllegalArgumentException(
            "Must have dot options to guess with."
        );
        if (!dups && len > oplen) throw new IllegalArgumentException(
            "Cant create solution of that length without duplicates."
        );

        this.dups = dups;
        this.len = len;
        this.nguesses = nguesses;
        this.options = options;
    }
}

// A list of guesses.
interface ILoGuess {
    WorldImage draw();
    ILoGuess addToIncomplete(Dot dot, int limit);
    ILoGuess dropFromIncomplete();
    ILoGuess commitIncomplete(int limit, ILoDot solution);
    ILoGuess commitIncompleteHelper(
        int limit,
        ILoDot solution,
        boolean premoved
    );
}

class ConsGuess implements ILoGuess {

    Guess guess;
    ILoGuess nxt;

    ConsGuess(Guess guess, ILoGuess nxt) {
        this.guess = guess;
        this.nxt = nxt;
    }

    public WorldImage draw() {
        return Util.pairGapAbove(guess.draw(), this.nxt.draw());
    }

    public ILoGuess addToIncomplete(Dot dot, int limit) {
        return new ConsGuess(this.guess, this.nxt.addToIncomplete(dot, limit));
    }

    public ILoGuess dropFromIncomplete() {
        return new ConsGuess(this.guess, this.nxt.dropFromIncomplete());
    }

    public ILoGuess commitIncomplete(int limit, ILoDot solution) {
        return this.commitIncompleteHelper(limit, solution, false);
    }

    public ILoGuess commitIncompleteHelper(
        int limit,
        ILoDot solution,
        boolean premoved
    ) {
        return new ConsGuess(
            this.guess,
            this.nxt.commitIncompleteHelper(limit, solution, premoved)
        );
    }
}

class ConsIncompleteGuess implements ILoGuess {

    IncompleteGuess guessSoFar;
    ILoGuess nxt;

    ConsIncompleteGuess(IncompleteGuess guessSoFar, ILoGuess nxt) {
        this.guessSoFar = guessSoFar;
        this.nxt = nxt;
    }

    public WorldImage draw() {
        return Util.pairGapAbove(guessSoFar.draw(), this.nxt.draw());
    }

    public ILoGuess addToIncomplete(Dot dot, int limit) {
        return new ConsIncompleteGuess(
            this.guessSoFar.tryAppend(dot, limit),
            this.nxt
        );
    }

    public ILoGuess dropFromIncomplete() {
        return new ConsIncompleteGuess(
            this.guessSoFar.tryRemoveLast(),
            this.nxt
        );
    }

    public ILoGuess commitIncomplete(int limit, ILoDot solution) {
        return this.commitIncompleteHelper(limit, solution, false);
    }

    public ILoGuess commitIncompleteHelper(
        int limit,
        ILoDot solution,
        boolean premoved
    ) {
        if (!premoved) throw new Error("Here");
        if (this.guessSoFar.full(limit)) {
            ILoDot guessDots = this.guessSoFar.guessSoFar;
            Feedback feedback = guessDots.compare(solution);
            return new ConsIncompleteGuess(
                new IncompleteGuess(new MtDot()),
                new ConsGuess(new Guess(guessDots, feedback), this.nxt)
            );
        } else return this;
    }
}

// Placeholder for guesses yet to be guessed.
class ConsPlaceholderGuess implements ILoGuess {

    ILoGuess nxt;

    ConsPlaceholderGuess(ILoGuess nxt) {
        this.nxt = nxt;
    }

    public WorldImage draw() {
        return Util.pairGapAbove(
            new RectangleImage(1, 2 * Dot.r, OutlineMode.SOLID, Color.BLACK),
            this.nxt.draw()
        );
    }

    // Fill a list with n placeholders.
    static ILoGuess mkN(int n) {
        if (n == 1) return new ConsIncompleteGuess(
            new IncompleteGuess(new MtDot()),
            new MtGuess()
        );
        return new ConsPlaceholderGuess(mkN(n - 1));
    }

    public ILoGuess addToIncomplete(Dot dot, int limit) {
        return new ConsPlaceholderGuess(this.nxt.addToIncomplete(dot, limit));
    }

    public ILoGuess dropFromIncomplete() {
        return new ConsPlaceholderGuess(this.nxt.dropFromIncomplete());
    }

    public ILoGuess commitIncompleteHelper(
        int limit,
        ILoDot solution,
        boolean premoved
    ) {
        if (!premoved) return this.nxt.commitIncompleteHelper(
                limit,
                solution,
                true
            );
        else return new ConsPlaceholderGuess(
            this.nxt.commitIncomplete(limit, solution)
        );
    }

    public ILoGuess commitIncomplete(int limit, ILoDot solution) {
        return this.commitIncompleteHelper(limit, solution, false);
    }
}

class MtGuess implements ILoGuess {

    public WorldImage draw() {
        return new EmptyImage();
    }

    public ILoGuess addToIncomplete(Dot dot, int limit) {
        return this;
    }

    public ILoGuess dropFromIncomplete() {
        return this;
    }

    public ILoGuess commitIncomplete(int limit, ILoDot solution) {
        return this;
    }

    public ILoGuess commitIncompleteHelper(
        int limit,
        ILoDot solution,
        boolean premoved
    ) {
        return this;
    }
}

// A guess.
class Guess {

    ILoDot guess; // The dots the user entered.
    Feedback feedback; // The feedback returned.

    Guess(ILoDot guess, Feedback feedback) {
        this.guess = guess;
        this.feedback = feedback;
    }

    WorldImage draw() {
        return Util.pairGap(this.guess.draw(), this.feedback.draw());
    }
}

// A guess in the midst of being entered.
class IncompleteGuess {

    ILoDot guessSoFar;

    IncompleteGuess(ILoDot guessSoFar) {
        this.guessSoFar = guessSoFar;
    }

    WorldImage draw() {
        return guessSoFar.draw();
    }

    // Append the dot, unless length exceeds limit.
    IncompleteGuess tryAppend(Dot dot, int limit) {
        if (this.guessSoFar.len() + 1 <= limit) return new IncompleteGuess(
            this.guessSoFar.append(dot)
        );
        else return this;
    }

    //Remove the last dot.
    IncompleteGuess tryRemoveLast() {
        int len = this.guessSoFar.len();
        if (len == 0) return this;
        else return new IncompleteGuess(this.guessSoFar.remove(len - 1));
    }

    // Is the guess full?
    boolean full(int limit) {
        return this.guessSoFar.len() == limit;
    }
}

// Feedback for a guess.
class Feedback {

    int exact, inexact;

    Feedback(int exact, int inexact) {
        this.exact = exact;
        this.inexact = inexact;
    }

    Feedback add(Feedback other) {
        int otherExact = other.exact, otherInexact = other.inexact;

        return new Feedback(
            this.exact + otherExact,
            this.inexact + otherInexact
        );
    }

    WorldImage draw() {
        return Util.strPair(
            String.valueOf(this.exact),
            String.valueOf(this.exact)
        );
    }
}

// A list of dots.
interface ILoDot {
    int len(); // Get length.

    Dot get(int n); // Get nth element.

    ILoDot remove(int n); // Remove nth element.

    ILoDot removeAll(ILoInt indices); // Remove element at each index.

    ILoDot gen(int n); // Generate randomized list.

    boolean match(Color col); // Do the colors match?

    boolean in(Dot dot); // Is the dot in here?

    Feedback compare(ILoDot other); // Compare two lists & give feedback.

    ILoInt exactIndices(ILoDot other); // Get indices of exact matches.

    ILoInt exactIndicesHelper(ILoDot other, int i);

    int countInexact(ILoDot other); // Get the number of inexact matches. Must be fed exact match-free lists to be
    // accurate.

    int countInexactHelper(ILoDot other, ILoDot seen);

    WorldImage draw(); // Draw the dots.

    int getW(); // Get the total width, in pixels, of the list.

    ILoDot append(Dot dot);
}

class ConsDot implements ILoDot {

    Random rand;
    Dot dot;
    ILoDot nxt;

    ConsDot(Dot dot, ILoDot nxt, Random rand) {
        this.rand = rand;
        this.dot = dot;
        this.nxt = nxt;
    }

    ConsDot(Dot dot, ILoDot nxt) {
        this(dot, nxt, new Random());
    }

    public ILoDot append(Dot dot) {
        return new ConsDot(this.dot, this.nxt.append(dot), this.rand);
    }

    public ILoDot gen(int n) {
        return n <= 0
            ? new MtDot()
            : new ConsDot(
                this.get(this.rand.nextInt(this.len())),
                this.gen(n - 1)
            );
    }

    public Dot get(int n) {
        if (n == 0) return this.dot;
        else return this.nxt.get(n - 1);
    }

    public boolean in(Dot dot) {
        return dot.equals(this.dot) || this.nxt.in(dot);
    }

    public int len() {
        return 1 + this.nxt.len();
    }

    public boolean match(Color col) {
        return col.equals(this.dot.c);
    }

    public WorldImage draw() {
        return new BesideAlignImage(
            AlignModeY.PINHOLE,
            this.dot.draw(),
            Util.gap,
            this.nxt.draw()
        );
    }

    public int getW() {
        return this.len() * Dot.r * 2;
    }

    public Feedback compare(ILoDot other) {
        if (this.len() != other.len()) throw new IllegalArgumentException(
            "Cannot compare different lengthed lists."
        );

        ILoInt exactIndices = this.exactIndices(other);

        ILoDot thisWithoutExact = this.removeAll(exactIndices);
        ILoDot otherWithoutExact = other.removeAll(exactIndices);

        int exact = exactIndices.len();
        int inexact = thisWithoutExact.countInexact(otherWithoutExact);

        return new Feedback(exact, inexact);
    }

    public ILoInt exactIndices(ILoDot other) {
        return this.exactIndicesHelper(other, 0);
    }

    public ILoInt exactIndicesHelper(ILoDot other, int i) {
        // Stop after reaching the end of the list.
        if (i == this.len()) return new MtInt();

        return this.get(i).equals(other.get(i))
            ? new ConsInt(i, this.exactIndicesHelper(other, i + 1))
            : this.exactIndicesHelper(other, i + 1);
    }

    public int countInexact(ILoDot other) {
        return this.countInexactHelper(other, new MtDot());
    }

    public int countInexactHelper(ILoDot other, ILoDot seen) {
        if (seen.in(this.dot)) {
            return this.nxt.countInexactHelper(
                    other,
                    new ConsDot(this.dot, seen)
                );
        }

        return (
            (other.in(this.dot) ? 1 : 0) +
            this.nxt.countInexactHelper(other, new ConsDot(this.dot, seen))
        );
    }

    public ILoDot remove(int n) {
        if (n < 0) throw new IllegalArgumentException(
            "Indices must be positive."
        );

        if (n == 0) return this.nxt;
        else return new ConsDot(this.dot, this.nxt.remove(n - 1));
    }

    public ILoDot removeAll(ILoInt indices) {
        if (indices.isEmpty()) return this;
        return this.remove(indices.first()).removeAll(indices.rest().subOne());
    }
}

class MtDot implements ILoDot {

    public ILoDot append(Dot dot) {
        return new ConsDot(dot, this);
    }

    public ILoDot gen(int n) {
        return new MtDot();
    }

    public Dot get(int n) {
        throw new IllegalArgumentException("Index out of bounds.");
    }

    public int len() {
        return 0;
    }

    public WorldImage draw() {
        return new EmptyImage();
    }

    public int getW() {
        return 0;
    }

    public Feedback compare(ILoDot other) {
        return new Feedback(0, 0);
    }

    public boolean match(Color col) {
        return false;
    }

    public boolean in(Dot dot) {
        return false;
    }

    public ILoInt exactIndices(ILoDot other) {
        throw new IllegalArgumentException(
            "Empty list doesn't exactly match anything."
        );
    }

    public ILoInt exactIndicesHelper(ILoDot other, int i) {
        return new MtInt();
    }

    public int countInexact(ILoDot other) {
        return 0;
    }

    public int countInexactHelper(ILoDot other, ILoDot seen) {
        return 0;
    }

    public ILoDot remove(int n) {
        throw new IllegalArgumentException("Index out of bounds.");
    }

    public ILoDot removeAll(ILoInt indices) {
        return this;
    }
}

// A dot.
class Dot {

    static int r = 16 * Util.scale;
    static OutlineMode outlineMode = OutlineMode.SOLID;

    Color c;

    Dot(Color c) {
        this.c = c;
    }

    // Draw the dot.
    WorldImage draw() {
        return new CircleImage(r, outlineMode, this.c);
    }

    // Get the width of the dot.
    int getW() {
        return 2 * r;
    }
}

// A list of integers.
interface ILoInt {
    int len(); // Get the length of the list.

    ILoInt remove(int n); // Remove the nth element of the list.

    boolean isEmpty(); // Is it empty?

    int first(); // Get the first value.

    ILoInt rest(); // Get the rest value.

    ILoInt subOne(); // Subtract 1 from every int.
}

class ConsInt implements ILoInt {

    int val;
    ILoInt nxt;

    ConsInt(int val, ILoInt nxt) {
        this.val = val;
        this.nxt = nxt;
    }

    public int len() {
        return 1 + this.nxt.len();
    }

    public ILoInt remove(int n) {
        if (n < 0) throw new IllegalArgumentException(
            "Indices must be positive."
        );
        return n == 0
            ? this.nxt
            : new ConsInt(this.val, this.nxt.remove(n - 1));
    }

    public boolean isEmpty() {
        return false;
    }

    public int first() {
        return this.val;
    }

    public ILoInt rest() {
        return this.nxt;
    }

    public ILoInt subOne() {
        return new ConsInt(this.val - 1, this.nxt.subOne());
    }
}

class MtInt implements ILoInt {

    public int len() {
        return 0;
    }

    public ILoInt remove(int n) {
        throw new IllegalArgumentException(
            "Index out of bounds: cannot remove something from nothing."
        );
    }

    public boolean isEmpty() {
        return true;
    }

    public int first() {
        throw new IllegalArgumentException(
            "Cannot get first element in a list without any."
        );
    }

    public ILoInt rest() {
        throw new IllegalArgumentException("Cannot get rest of empty list.");
    }

    public ILoInt subOne() {
        return this;
    }
}
