package mastermind;

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

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

    Dot redDot = new Dot(Color.red),
        greenDot = new Dot(Color.green),
        blueDot = new Dot(Color.blue),
        reenDot = new Dot(new Color(255, 255, 0));
    
    ILoDot noDot = new MtDot(),
           dotsOne = new ConsDot(redDot, new ConsDot(greenDot, new ConsDot(blueDot, noDot)), new Random(0)),
           dotsTwo = new ConsDot(redDot, new ConsDot(blueDot, new ConsDot(blueDot, noDot))),
           dotsThree = new ConsDot(reenDot, new ConsDot(reenDot, new ConsDot(reenDot, noDot))),
           exampleDotsOne = new ConsDot(redDot, new ConsDot(greenDot, new ConsDot(blueDot, new ConsDot(greenDot, new ConsDot(redDot, new ConsDot(greenDot, noDot)))))),
           exampleDotsTwo = new ConsDot(redDot, new ConsDot(blueDot, new ConsDot(greenDot, new ConsDot(greenDot, new ConsDot(blueDot, new ConsDot(blueDot, noDot)))))),
           exampleDotsOneNoExact = new ConsDot(greenDot, new ConsDot(blueDot, new ConsDot(redDot, new ConsDot(greenDot, noDot)))),
           exampleDotsTwoNoExact = new ConsDot(blueDot, new ConsDot(greenDot, new ConsDot(blueDot, new ConsDot(blueDot, noDot))));
    
    Feedback exampleFeedback = new Feedback(2, 2);
    
    Game exampleGame = new Game();
    
    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));
    }
    
    boolean testDrawMethods(Tester t) {
        WorldCanvas c = new WorldCanvas(1000, 1000);
        WorldScene s = new WorldScene(1000, 1000);
        
        IncompleteGuess incomplete = new IncompleteGuess(exampleDotsOne);
        
        return
                        c.drawScene(s.placeImageXY(incomplete.draw(), incomplete.getW()/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;
    }
}

class Util {
    static int scale = 2;
    static int fontSz = 16 * 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 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 {
    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;
    int guessesLeft;
    ILoGuess guesses;
    
    Game(GameConf conf, int guessesLeft, ILoGuess guesses) {
        this.conf = conf;
        this.guessesLeft = guessesLeft;
        this.guesses = guesses;
    }
    
    // Convenience constructor using default config.
    Game() { this(DEFAULTCONF, DEFAULTCONF.nguesses, new MtGuess()); }
    
    WorldImage draw() {
        return new EmptyImage();
    }
}

// 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 {}

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

class MtGuess implements ILoGuess {}

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;
    }
}

class IncompleteGuess {
    ILoDot guessSoFar;
    
    IncompleteGuess(ILoDot guessSoFar) {
        this.guessSoFar = guessSoFar;
    }
    
    WorldImage draw() {
        return Util.pairGap(guessSoFar.draw(), Util.strPair("-", "-"));
    }
    
    int getW() {
        return this.guessSoFar.getW() + 2 * Util.gapW + 2 * Util.fontSz;
    }
}

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() {
        WorldImage exactDigit = new TextImage(String.valueOf(this.exact), 16 * Util.scale, FontStyle.BOLD, Color.BLACK);
        WorldImage inexactDigit = new TextImage(String.valueOf(this.inexact), 16 * Util.scale, FontStyle.BOLD, Color.BLACK);

        return new BesideAlignImage(AlignModeY.MIDDLE, exactDigit, Util.gap, inexactDigit);
    }
    
    int getW() {
        return 2 * 16 * Util.scale + Util.gapW;
    }
}

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.
}

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 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 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; }
}

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; }
}