
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

public class Perceptron {

    /**
     * Zoznam treningovych prikladov
     */
    private List<Sample> samples = new LinkedList<Sample>();

    /**
     * Vahy
     */
    private double[] weights;

    /**
     * Uciaci koeficient rho.
     */
    private double learningRate = 0.5;


    /**
     * Prida novy pozitivny priklad s danym vstupnym vektorom.
     * @param inputVector vstupny vektor
     */
    public void addPositiveSample(double... inputVector) {
        addSample(1, inputVector);
    }

    /**
     * Prida novy negativny priklad s danym vstupnym vektorom.
     * @param inputVector vstupny vektor
     */
    public void addNegativeSample(double... inputVector) {
        addSample(-1, inputVector);
    }

    /**
     * Prida do vzorky novy priklad s danym vstupnym vektorom a ocakavanym vysledkom.
     * @param expectedResult ocakavany vysledok klasifikacie
     * @param inputVector vstupny vektor
     */
    private void addSample(int expectedResult, double... inputVector) {
        initializeWeightsIfNecessary(inputVector.length);

        Sample sample = new Sample();
        sample.setExpectedOutput(expectedResult);
        sample.setInputVector(inputVector);

        samples.add(sample);
    }

    /**
     * Spusti trenovanie perceptronu na danych prikladoch.
     */
    public void train() {
        int correctSamples = 0;
        int sampleIndex = 0;
        while (correctSamples < samples.size()) {
            Sample currentSample = samples.get(sampleIndex);

            int perceptronOutput = evaluateTrainingRule(currentSample.getInputVector());

            System.out.println("input vector = " + Arrays.toString(currentSample.getInputVector())
                    + "; expected = " + currentSample.getExpectedOutput()
                    + "; calculated = " + perceptronOutput);

            adjustWeights(currentSample, perceptronOutput);

            System.out.println("Adjusted weights w = " + Arrays.toString(weights));

            /* Ak sa vystup perceptronu rovnal ocakavanemu vystupu (y=d),
             * tak zvysime pocitadlo bezchybnych testov.
             */
            if (perceptronOutput == currentSample.getExpectedOutput()) {
                correctSamples++;
            } /* Ak sa nerovnal, tak ho vynulujeme */ else {
                correctSamples = 0;
            }

            System.out.println("Sample ID = " + sampleIndex + "; Correct = " + correctSamples);

            /* Zabezpecime, aby sme sa po dosiahnuti konca treningovych sad
             * dostali opat na ich zaciatok.
             */
            sampleIndex++;
            if (sampleIndex >= samples.size()) {
                sampleIndex = 0;
            }
            System.out.println("---");
        }
        /* Nakoniec vypiseme vysledne vahy */
        System.out.println("Vysledne vahy: w = " + Arrays.toString(weights));
    }

    /**
     * Upravi vahy na zaklade vypoctu nad danou vzorkou
     * @param currentSample vzorka, nad ktorou sa vyrataval vysledok perceptronu
     * @param perceptronOutput vysledok vypoctu perceptronu
     */
    private void adjustWeights(Sample currentSample, int perceptronOutput) {
        double[] currentInputVector = currentSample.getInputVector();
        for (int i = 0; i < currentInputVector.length; i++) {
            weights[i] += learningRate * (currentSample.getExpectedOutput() - perceptronOutput) * currentInputVector[i];
        }
    }

    /**
     * Vyhodnoti uciace pravidlo perceptronu
     * @param inputVector vstupny vektor
     * @return vysledok uciaceho pravidla
     */
    private int evaluateTrainingRule(double[] inputVector) {
        if (inputVector.length != weights.length) {
            throw new ArrayIndexOutOfBoundsException("Mismatched input vector and weights length.");
        }
        double sum = 0;
        for (int i = 0; i < inputVector.length; i++) {
            sum += inputVector[i] * weights[i];
        }
        return sum >= 0 ? 1 : -1;
    }

    /**
     * Vrati klasifikaciu pre vstupny vektor na zaklade treningovych prikladov.
     * @param inputVector
     * @return vysledok klasifikacie: 1 pre kladny priklad, -1 pre zaporny
     */
    public int classify(double... inputVector) {
        return evaluateTrainingRule(inputVector);
    }

    /**
     * Inicializuje vektor vah, ak este nie je inicializovany.
     * @param vectorLength dlzka vektora vah.
     */
    private void initializeWeightsIfNecessary(int vectorLength) {
        if(weights == null) {
            weights = new double[vectorLength];
        }
    }

    private static class Sample {

        /**
         * Vstupny vektor treningoveho prikladu.
         */
        private double[] inputVector;
        /*
         * Klasifikacia prikladu, obvykle plati: 1 = kladny priklad, -1 = zaporny priklad
         */
        private int expectedOutput;

        public void setInputVector(double... inputVector) {
            if (inputVector[0] != -1) {
                throw new IllegalArgumentException("First element must be -1");
            }

            this.inputVector = inputVector;
        }

        public void setExpectedOutput(int expectedOutput) {
            this.expectedOutput = expectedOutput;
        }

        public int getExpectedOutput() {
            return expectedOutput;
        }

        public double[] getInputVector() {
            return inputVector;
        }
    }

    public static void main(String[] args) {
        Perceptron p = new Perceptron();
        
        p.addPositiveSample(-1, 1, 2);
        p.addPositiveSample(-1, 2, 1);
        
        p.addNegativeSample(-1, 4, 2);
        p.addNegativeSample(-1, 4, 1);

        p.train();

        System.out.println(p.classify(-1, 1, 2));
        System.out.println(p.classify(-1, 7, 5));
        System.out.println(p.classify(-1, 2, 2));
    }
}
