Day3.java

package com.roxoft.aoc.y2015.Day3;

import com.roxoft.aoc.UnexpectedSolutionException;
import com.roxoft.lib.IntegerCoord2D;
import com.roxoft.lib.Movable2DEntity;

import java.util.ArrayList;
import java.util.List;

public final class Day3 {
    /** List of instructions to follow. */
    private final String instructions;
    /** The {@link List} of {@link Movable2DEntity}s that will be visiting houses. */
    private final List<Movable2DEntity> santas;
    /** Which {@link Movable2DEntity} is receiving the next instruction. **/
    private int santaFocus = 0;

    /**
     * @param santaNumber the number of the desired {@link Movable2DEntity}.
     * @return The {@link Movable2DEntity} identified by the provided santaNumber
     */
    public Movable2DEntity getSanta(final int santaNumber) {
        return santas.get(santaNumber);
    }

    private Day3(final String instructionSequence) {
        instructions = instructionSequence;
        santas = new ArrayList<>();
    }

    private Day3(final String instructionSequence, final List<Movable2DEntity> predefinedSantas) {
        instructions = instructionSequence;
        santas = predefinedSantas;
    }

    /**
     * @param instructionSequence A {@link String} representing a series of instructions
     *
     * @return A {@link Day3} object ready for solving the problem.
     */
    public static Day3 of(final String instructionSequence) {
        return new Day3(instructionSequence);
    }

    /**
     * @return A new {@link Day3} object that is a clone of this one with an additional {@link Movable2DEntity} at {@link IntegerCoord2D} (0,0)
     */
    public Day3 withNewSanta() {
        return withASantaAt(IntegerCoord2D.of(0, 0));
    }

    /**
     * @param location the {@link IntegerCoord2D} location of the new {@link Movable2DEntity}
     *
     * @return A new {@link Day3} object that is a clone of this one with an additional {@link Movable2DEntity} at the provided {@link IntegerCoord2D} location
     */
    public Day3 withASantaAt(final IntegerCoord2D location) {
        final List<Movable2DEntity> newSantaList = santas;
        newSantaList.add(Movable2DEntity.at(location));
        return new Day3(
                instructions,
                newSantaList
        );
    }

    /**
     * @return The number of unique houses visited by all {@link Movable2DEntity}s currently
     */
    public long getNumberOfVisitedHouses() {
        return santas
                .stream()
                .map(Movable2DEntity::locationHistory)
                .flatMap(List::stream)
                .distinct()
                .count();
    }

    /**
     * @return An updated {@link Day3} object where all instructions have been followed
     */
    public Day3 followInstructions() {
        for (int i = 0; i < instructions.length(); i++) {
            followInstruction(i);
        }
        return this;
    }

    /**
     * @param i the instruction index to follow
     * @return An updated {@link Day3} object where the indicated instruction has been followed
     */
    public Day3 followInstruction(final int i) {
        final Movable2DEntity nextSanta = santas.get(santaFocus);

        switch (instructions.charAt(i)) {
            case '^': nextSanta.moveUp(); break;
            case 'v': nextSanta.moveDown(); break;
            case '>': nextSanta.moveRight(); break;
            case '<': nextSanta.moveLeft(); break;
            default: throw new UnexpectedSolutionException("Unexpected instruction '" + instructions.charAt(0) + "'");
        }

        //Cycle through Santas
        santaFocus = (santaFocus + 1) < santas.size() ? (santaFocus + 1) : 0;
        return this; //XXX: Immutable classes would probably be better.
    }

    /**
     * @return An updated {@link Day3} object where the next instruction has been followed
     */
    public Day3 followNextInstruction() {
        return followInstruction(0);
    }
}