package trees;

import java.awt.Color;
import tester.Tester;

class Examples {
    IBlob red = new Solid("red", new Color(255, 0, 0));
    IBlob green = new Solid("green", new Color(0, 255, 0));
    IBlob blue = new Solid("blue", new Color(0, 0, 255));
    IBlob brown = new Solid("brown", new Color(96, 61, 14));
    IBlob brightBrown = new Combo("brown", new Brighten(brown));
    IBlob darkBrown = new Combo("brown", new Darken(brown));
    IBlob neon = new Solid("neon", new Color(255, 255, 255));
    IBlob darkNeon = new Combo("neon", new Darken(neon));
    IBlob brightNeon = new Combo("neon", new Brighten(neon));
    IBlob neonBrown = new Combo("neon brown", new Blend(neon, brown));
    IBlob darkNeonDarkBrown = new Combo("neon brown", new Blend(darkBrown, darkNeon));
    IBlob brightNeonBrightBrown = new Combo("neon brown", new Blend(brightBrown, brightNeon));
    IBlob purple = new Combo("purple", new Blend(red, blue));
    IBlob darkPurple = new Combo("dark purple", new Darken(purple));
    IBlob khaki = new Combo("khaki", new Blend(red, green));
    IBlob yellow = new Combo("yellow", new Brighten(khaki));
    IBlob mauve = new Combo("mauve", new Blend(purple, khaki));
    IBlob pink = new Combo("pink", new Brighten(mauve));
    IBlob coral = new Combo("coral", new Blend(pink, khaki));
    
    boolean testIMixtureApply(Tester t) {
        return t.checkExpect(new Darken(red).apply(), Color.red.darker()) &&
                        t.checkExpect(new Brighten(green).apply(), Color.green.brighter()) &&
                        t.checkExpect(new Blend(red, green).apply(), new Color(127, 127, 0));
    }
    
    boolean testIBlobGetFinalColor(Tester t) {
        return t.checkExpect(red.getFinalColor(), Color.red) &&
                        t.checkExpect(purple.getFinalColor(), new Color(127, 0, 127));
    }
    
    boolean testIMixtureCountPaints(Tester t) {
        return t.checkExpect(new Blend(neon, brown).countPaints(), 2) &&
                        t.checkExpect(new Darken(brown).countPaints(), 2);
    }
    
    boolean testIBlobCountPaints(Tester t) {
        return t.checkExpect(red.countPaints(), 1) &&
                        t.checkExpect(purple.countPaints(), 2) &&
                        t.checkExpect(mauve.countPaints(), 4);
    }
    
    boolean testIMixtureFormulaDepth(Tester t) {
        return t.checkExpect(new Blend(neon, purple).formulaDepth(), 2) &&
                        t.checkExpect(new Darken(neon).formulaDepth(), 1);
    }
    
    boolean testIBlobFormulaDepth(Tester t) {
        return t.checkExpect(red.formulaDepth(), 0) &&
                        t.checkExpect(purple.formulaDepth(), 1) &&
                        t.checkExpect(darkPurple.formulaDepth(), 2) &&
                        t.checkExpect(coral.formulaDepth(), 4);
    }
    
    boolean testIMixtureInvert(Tester t) {
        return t.checkExpect(new Blend(neon, brown).invert(), new Blend(neon, brown)) &&
                        t.checkExpect(new Darken(brown).invert(), new Brighten(brown)) &&
                        t.checkExpect(new Darken(new Combo("bright brown", new
                        Brighten(brown))).invert(), new Brighten(new
                        Combo("bright brown", new Darken(brown))));
    }
    
    boolean testIBlobInvert(Tester t) {
        return t.checkExpect(red.invert(), red) &&
                        t.checkExpect(brightBrown.invert(), darkBrown) &&
                        t.checkExpect(brightNeonBrightBrown.invert(), darkNeonDarkBrown);
                        
    }
    
    boolean testIBlobMixingFormula(Tester t) {
        return t.checkExpect(red.mixingFormula(0), "red") &&
                        t.checkExpect(red.mixingFormula(1000), "red") &&
                        t.checkExpect(pink.mixingFormula(0), "pink") &&
                        t.checkExpect(pink.mixingFormula(1), "(brighten mauve)") &&
                        t.checkExpect(pink.mixingFormula(2), "(brighten (blend purple khaki))") &&
                        t.checkExpect(pink.mixingFormula(3), "(brighten (blend (blend red blue) (blend red green)))") &&
                        t.checkExpect(pink.mixingFormula(300), "(brighten (blend (blend red blue) (blend red green)))");
    }
}

interface IBlob {
    // Calculate the final color.
    Color getFinalColor();
    
    // Count how many solids were used.
    int countPaints();
    
    // Find the maximum number of blends used in a tree path.
    int formulaDepth();
    
    // Swap all darkens and brightens.
    IBlob invert();
    
    // Returns a string describing the steps to create the color, to a certain depth.
    String mixingFormula(int depth);
}

class Solid implements IBlob {
    String name;
    Color color;
    
    Solid(String name, Color color) {
        this.name = name;
        this.color = color;
    }
    
    public Color getFinalColor() { return this.color; }
    
    public int countPaints() { return 1; }
    
    public int formulaDepth() { return 0; }
    
    public IBlob invert() { return this; }
    
    public String mixingFormula(int _depth) { return this.name; }
}

class Combo implements IBlob {
    String name;
    IMixture mixture;
    
    Combo(String name, IMixture mixture) {
        this.name = name;
        this.mixture = mixture;
    }
    
    public Color getFinalColor() {
        return this.mixture.apply();
    }
    
    public int countPaints() {
        return this.mixture.countPaints();
    }
    
    public int formulaDepth() {
        return this.mixture.formulaDepth();
    }
    
    public IBlob invert() {
        return new Combo(this.name, this.mixture.invert());
    }
    
    public String mixingFormula(int depth) {
        return depth <= 0 ? this.name : this.mixture.mixingFormula(depth);
    }
}

interface IMixture {
    // Apply a mixture.
    Color apply();
    
    // Count how many solid IBlobs were used.
    int countPaints();
    
    // Count the deepest the mixtures are nested.
    int formulaDepth();
    
    // Swap all darkens and brightens.
    IMixture invert();
    
    String mixingFormula(int depth);
}

class Darken implements IMixture {
    IBlob color;
    
    Darken(IBlob color) {
        this.color = color;
    }
    
    public Color apply() {
        return this.color.getFinalColor().darker();
    }
    
    public int countPaints() {
        // Adds black paint.
        return this.color.countPaints() + 1;
    }
    
    public int formulaDepth() {
        return 1 + this.color.formulaDepth();
    }
    
    public IMixture invert() {
        return new Brighten(this.color.invert());
    }
    
    public String mixingFormula(int depth) {
        return "(darken ".concat(this.color.mixingFormula(depth - 1)).concat(")");
    }
}

class Brighten implements IMixture {
    IBlob color;
    
    Brighten(IBlob color) {
        this.color = color;
    }
    
    public Color apply() {
        return this.color.getFinalColor().brighter();
    }
    
    public int countPaints() {
        // Adds white paint.
        return this.color.countPaints() + 1;
    }
    
    public int formulaDepth() {
        return 1 + this.color.formulaDepth();
    }
    
    public IMixture invert() {
        return new Darken(this.color.invert());
    }

    public String mixingFormula(int depth) {
        return "(brighten ".concat(this.color.mixingFormula(depth - 1)).concat(")");
    }
}

class Blend implements IMixture {
    IBlob colorOne;
    IBlob colorTwo;
    
    Blend(IBlob colorOne, IBlob colorTwo) {
        this.colorOne = colorOne;
        this.colorTwo = colorTwo;
    }
    
    public Color apply() {
        Color finalColorOne = this.colorOne.getFinalColor();
        Color finalColorTwo = this.colorTwo.getFinalColor();
        
        int redOne = finalColorOne.getRed();
        int redTwo = finalColorTwo.getRed();
        int greenOne = finalColorOne.getGreen();
        int greenTwo = finalColorTwo.getGreen();
        int blueOne = finalColorOne.getBlue();
        int blueTwo = finalColorTwo.getBlue();
        
        return new Color(redOne/2 + redTwo/2, greenOne/2 + greenTwo/2, blueOne/2 + blueTwo/2);
    }
    
    public int countPaints() {
        return this.colorOne.countPaints() + this.colorTwo.countPaints();
    }
    
    public int formulaDepth() {
        return 1 + Math.max(this.colorOne.formulaDepth(), this.colorTwo.formulaDepth());
    }
    
    public IMixture invert() {
        return new Blend(this.colorOne.invert(), this.colorTwo.invert());
    }
    
    public String mixingFormula(int depth) {
        return "(blend ".concat(this.colorOne.mixingFormula(depth - 1))
                        .concat(" ")
                        .concat(this.colorTwo.mixingFormula(depth - 1))
                        .concat(")");
    }
}