package wordcram;

import java.awt.Color; // awt: turns P5-land color into awt color, for renderers
import java.awt.Shape; // awt: word->shape, skip if too small, pass it on
import java.awt.geom.Rectangle2D; // awt: to get bounding box width & height
import java.util.ArrayList;
import java.util.ListIterator;

import processing.core.PFont;
import processing.core.PVector;

class WordCramEngine {

    private WordRenderer renderer;

    private WordFonter fonter;
    private WordSizer sizer;
    private WordColorer colorer;
    private WordAngler angler;
    private WordPlacer placer;
    private WordNudger nudger;

    private ArrayList<EngineWord> eWords;
    private ListIterator<EngineWord> eWordIter;
    private ArrayList<EngineWord> drawnWords = new ArrayList<EngineWord>();
    private ArrayList<Word> skippedWords = new ArrayList<Word>();

    private RenderOptions renderOptions;
    private Observer observer;

    // TODO Damn, really need to break down that list of arguments.
    WordCramEngine(WordRenderer renderer, Word[] words, WordFonter fonter, WordSizer sizer, WordColorer colorer, WordAngler angler, WordPlacer placer, WordNudger nudger, WordShaper shaper, BBTreeBuilder bbTreeBuilder, RenderOptions renderOptions, Observer observer) {
        this.renderer = renderer;

        this.fonter = fonter;
        this.sizer = sizer;
        this.colorer = colorer;
        this.angler = angler;
        this.placer = placer;
        this.nudger = nudger;
        this.observer = observer;

        this.renderOptions = renderOptions;
        this.eWords = wordsIntoEngineWords(words, shaper, bbTreeBuilder);
        this.eWordIter = eWords.listIterator();
    }

    private ArrayList<EngineWord> wordsIntoEngineWords(Word[] words, WordShaper wordShaper, BBTreeBuilder bbTreeBuilder) {
        ArrayList<EngineWord> engineWords = new ArrayList<EngineWord>();

        int maxNumberOfWords = words.length;
        if (renderOptions.maxNumberOfWordsToDraw >= 0) {
            maxNumberOfWords = Math.min(maxNumberOfWords, renderOptions.maxNumberOfWordsToDraw);
        }

        for (int i = 0; i < maxNumberOfWords; i++) {
            Word word = words[i];
            EngineWord eWord = new EngineWord(word, i, words.length, bbTreeBuilder);

            PFont wordFont = word.getFont(fonter);
            float wordSize = word.getSize(sizer, i, words.length);
            float wordAngle = word.getAngle(angler);

            Shape shape = wordShaper.getShapeFor(eWord.word.word, wordFont, wordSize, wordAngle);
            if (isTooSmall(shape, renderOptions.minShapeSize)) {
                skipWord(word, WordSkipReason.SHAPE_WAS_TOO_SMALL);
            }
            else {
                eWord.setShape(shape, renderOptions.wordPadding);
                engineWords.add(eWord);  // DON'T add eWords with no shape.
            }
        }

        for (int i = maxNumberOfWords; i < words.length; i++) {
            skipWord(words[i], WordSkipReason.WAS_OVER_MAX_NUMBER_OF_WORDS);
        }

        return engineWords;
    }

    private boolean isTooSmall(Shape shape, int minShapeSize) {
        if (minShapeSize < 1) {
            minShapeSize = 1;
        }
        Rectangle2D r = shape.getBounds2D();

        // Most words will be wider than tall, so this basically boils down to height.
        // For the odd word like "I", we check width, too.
        return r.getHeight() < minShapeSize || r.getWidth() < minShapeSize;
    }

    private void skipWord(Word word, WordSkipReason reason) {
        // TODO delete these properties when starting a sketch, in case it's a re-run w/ the same words.
        // NOTE: keep these as properties, because they (will be) deleted when the WordCramEngine re-runs.
        word.wasSkippedBecause(reason);
        skippedWords.add(word);
        observer.wordSkipped(word);
    }

    boolean hasMore() {
        return eWordIter.hasNext();
    }

    void drawAll() {
    	observer.beginDraw();
    	while(hasMore()) {
            drawNext();
        }
        renderer.finish();
        observer.endDraw();
    }

    void drawNext() {
        if (!hasMore()) return;
        EngineWord eWord = eWordIter.next();
        boolean wasPlaced = placeWord(eWord);
        if (wasPlaced) { // TODO unit test (somehow)
            drawWordImage(eWord);
            observer.wordDrawn(eWord.word);
        }
    }

    private boolean placeWord(EngineWord eWord) {
        Word word = eWord.word;
        Rectangle2D rect = eWord.getShape().getBounds2D(); // TODO can we move these into EngineWord.setDesiredLocation? Does that make sense?
        int wordImageWidth = (int)rect.getWidth();
        int wordImageHeight = (int)rect.getHeight();

        eWord.setDesiredLocation(placer, eWords.size(), wordImageWidth, wordImageHeight, renderer.getWidth(), renderer.getHeight());

        // Set maximum number of placement trials
        int maxAttemptsToPlace = renderOptions.maxAttemptsToPlaceWord > 0 ?
                                    renderOptions.maxAttemptsToPlaceWord :
                                    calculateMaxAttemptsFromWordWeight(word);

        EngineWord lastCollidedWith = null;
        for (int attempt = 0; attempt < maxAttemptsToPlace; attempt++) {

            eWord.nudge(nudger.nudgeFor(word, attempt));

            PVector loc = eWord.getCurrentLocation();
            if (loc.x < 0 || loc.y < 0 || loc.x + wordImageWidth >= renderer.getWidth() || loc.y + wordImageHeight >= renderer.getHeight()) {
                continue;
            }

            if (lastCollidedWith != null && eWord.overlaps(lastCollidedWith)) {
                continue;
            }

            boolean foundOverlap = false;
            for (EngineWord otherWord : drawnWords) {
                if (eWord.overlaps(otherWord)) {
                    foundOverlap = true;
                    lastCollidedWith = otherWord;
                }
            }

            if (!foundOverlap) {
                eWord.finalizeLocation();
                return true;
            }
        }

        skipWord(eWord.word, WordSkipReason.NO_SPACE);
        return false;
    }

    private int calculateMaxAttemptsFromWordWeight(Word word) {
        return (int)((1.0 - word.weight) * 600) + 100;
    }

    private void drawWordImage(EngineWord word) {
        drawnWords.add(word);
        renderer.drawWord(word, new Color(word.word.getColor(colorer), true));
    }

    Word getWordAt(float x, float y) {
        int size = drawnWords.size() - 1;
        for (int i = size; i >= 0; i--) {
            EngineWord eWord = drawnWords.get(i);
            if (eWord.containsPoint(x, y)) {
                return eWord.word;
            }
        }
        return null;
    }

    Word[] getSkippedWords() {
        return skippedWords.toArray(new Word[0]);
    }

    float getProgress() {
        return (float) eWordIter.nextIndex() / eWords.size();
    }
}

@@ -5887,24 +5887,51 @@
  otherWord;%0A
+                    break;%0A
             

Performance tweak: break after finding a colliding word.