/*
 * Licensed to the Ted Dunning under one or more contributor license
 * agreements.  See the NOTICE file that may be
 * distributed with this work for additional information
 * regarding copyright ownership.  Ted Dunning licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.mapr.synth.drive;

import com.tdunning.math.stats.AVLTreeDigest;
import com.tdunning.math.stats.TDigest;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import processing.core.PApplet;
import processing.core.PFont;

import java.text.DecimalFormat;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Trails extends PApplet {
    Queue<State> input;
    private State old = null;
    private TDigest speedDistribution;
    private Random noise;
    private Stripchart speed;
    private Stripchart throttle;
    private Stripchart rpm;
    private int clicks;

    @Override
    public void setup() {
        ExecutorService pool = Executors.newFixedThreadPool(1);
        BlockingQueue<State> q = new ArrayBlockingQueue<>(2000);
        input = q;
        pool.submit(new Producer(q));
        speedDistribution = new AVLTreeDigest(300);
        noise = new Random();

        speed = new Stripchart(10, 430, 460, 80, 1, 0, 0, 90);
        rpm = new Stripchart(10, 520, 460, 80, 1, 0, 0, 2200);
        throttle = new Stripchart(10, 610, 460, 80, 1, 0, 0, 100);

        frameRate(15);
    }

    @Override
    public void settings() {
        super.settings();
        size(500, 700);
        clicks = 5;
    }

    boolean started = false;

    public static class Rect2D {
        private double x, y, w, h;

        public Rect2D(double x, double y, double w, double h) {
            if (w < 0) {
                x = x + w;
                w = -w;
            }
            if (h < 0) {
                y = y + h;
                h = -h;
            }
            this.x = x;
            this.y = y;
            this.h = h;
            this.w = w;
        }

        public boolean contains(double x1, double y1) {
            boolean xwise = x1 >= x && x1 < x + w;
            boolean ywise = y1 >= y && y1 < y + h;
            return xwise && ywise;
        }
    }


    private Rect2D drawText(float x, float y, String format, Object... args) {
        String s = String.format(format, args);
        float w = textWidth(s);
        float up = textAscent();
        float down = textDescent();
        float width = w * 1.1F;
        float height = (up + down) * 1.1F;
//        rect(x, y + down, width, height);

        stroke(0, 0, 0, 100);
        fill(0, 0, 0, 100);
        text(s, x, y);
        return new Rect2D(x, y + down, width, -height);
    }

    @Override
    public void draw() {
        colorMode(RGB, 256);
        if (old != null) {
            // status displays at the top
            fill(0xee + dither(5), 0xee + dither(5), 0xff + dither(5), 20);
            stroke(0, 0, 0, 100);
            textSize(20F);
            fill(0, 0, 0, 100);
            Rect2D faster = drawText(300, 30, "Faster");
            Rect2D slower = drawText(300, 60, "Slower");
            if (mousePressed) {
                System.out.printf("%d %d\n", mouseX, mouseY);
                if (faster.contains(mouseX, mouseY)) {
                    clicks++;
                    if (clicks > 60) {
                        clicks = 60;
                    }
                    System.out.printf("%d\n", clicks);
                } else if (slower.contains(mouseX, mouseY)) {
                    clicks--;
                    if (clicks < 0) {
                        clicks = 0;
                    }
                    System.out.printf("%d\n", clicks);
                }
            }

//            fill(0xee + dither(5), 0xee + dither(5), 0xff + dither(5), 60);
//            drawText(250, 50, "%8.1f %8.1f", old.here.getX(), old.here.getY());
//            fill(0xee + dither(5), 0xee + dither(5), 0xff + dither(5), 60);
//            drawText(50, 50, "%8.0f %8.1f", old.car.getRpm(), old.car.getSpeed() / Constants.MPH);

            speed.display();
            rpm.display();
            throttle.display();
        }

        if (clicks > 0) {
            //Fade everything which is drawn
            if (frameCount % 10 == 0) {
//                noStroke();
//            fill(0xee + dither(5), 0xee + dither(5), 0xfe + dither(5), 3);
                colorMode(HSB, 100);
                stroke(0, 0, 0, 100);
                fill(0F, 0F, 120 + dither(11), 6F);
                rect(0, 90, width, height - 380);
            }

            translate(50, 390);
            scale(-30F, -30F);
            started = true;

            colorMode(HSB, 100);

            double meanSpeed = 0;

            noStroke();
            fill(25, 100, 80);
            ellipse(0, 0, 0.3F, 0.3F);

            fill(0, 100, 80);
            ellipse(-12, 7, 0.3F, 0.3F);

            for (int i = 0; !input.isEmpty() && i < clicks; i++) {
                State state = input.remove();
                Vector3D p = state.here;
                if (old != null) {
                    stroke(0, 0, 0);
                    strokeWeight(.1F);
                    double stepSize = old.here.subtract(p).getNorm();
                    if (stepSize < 10) {
                        meanSpeed += (old.car.getSpeed() - meanSpeed) * 0.4;
                        speedDistribution.add(meanSpeed);
//                        double hue = speedDistribution.cdf(old.car.getSpeed());
                        double hue = 100 * Math.pow(old.car.getSpeed() / Constants.MPH, 2) / Math.pow(100, 2);
                        stroke((float) hue, 70, 80);
                        line((float) old.here.getX(), (float) old.here.getY(), (float) p.getX(), (float) p.getY());
                    }
                    speed.addData((float) (old.car.getSpeed() / Constants.MPH));
                    rpm.addData((float) (old.car.getRpm()));
                    throttle.addData((float) old.car.getThrottle());
                }

                old = state;
            }
        }
    }

    private void fill(double c1, double c2, double c3, double alpha) {
        fill((float) c1, (float) c2, (float) c3, (float) alpha);
    }

    private double dither(double size) {
        return size * (noise.nextDouble() - noise.nextDouble());
    }

    public static void main(String[] args) {
        PApplet.main("com.mapr.synth.drive.Trails", new String[]{""});
    }


    public static class State {
        private final Engine car;
        private final Vector3D here;

        public State(Engine car, Vector3D here) {
            this.car = car;
            this.here = here;
        }
    }

    /**
     * Draws a stripchart recorder.
     */
    class Stripchart {
        int x;           // horizontal position of chart
        int y;           // vertical position of chart
        int nSamples;    // number of samples to display (affects width)
        int h;           // height of chart
        int colour;    // color of dots to plot
        int dataPos;     // where does next data point go?
        int startPos;    // where do we start plotting?
        int nPoints;     // number of points currently in the array
        int period;      // how often to draw a gray line
        double minValue;  // minimum value to display
        double maxValue;  // maximum value to display
        float[] points;  // the data points to plot

        private DecimalFormat d = new DecimalFormat("0.#");
        private String minString;  // minimum value as a string
        private String maxString;  // maximum value as a string

        private PFont legendFont = createFont("Arial", 10);
        private float prevX;    // remember previous point
        private float prevY;

        /*
          rightSpace tells how much room there is for the chart
          legend. VSPACE and HSPACE give the spacing from the border
          of the stripchart to the point plotting area.
        */
        private float rightSpace = 0;
        final static int VSPACE = 2;
        final static int HSPACE = 2;

        Stripchart(int x, int y, int nSamples, int h, int period, int c,
                   double minValue, double maxValue) {
            this.x = x;
            this.y = y;
            this.nSamples = nSamples;
            this.h = h;
            this.period = period;
            this.colour = c;
            // make sure minimum and max are in proper order
            this.minValue = Math.min(minValue, maxValue);
            this.maxValue = Math.max(minValue, maxValue);

            // and convert them to a string with minimal number of decimal places
            this.minString = d.format(minValue);
            this.maxString = d.format(maxValue);
            nPoints = 0;
            dataPos = 0;
            startPos = 0;
            points = new float[nSamples];
        }

        Stripchart(int x, int y, int w, int h, int period, int c) {
            this(x, y, w, h, period, c, h / 2.0, -h / 2.0);
        }

        /**
         * Add a data point to be plotted.
         * At this point you may be wondering why I am using an array
         * instead of an ArrayList. Although programmaticaly it may
         * be easier to add a new value to the list and remove the
         * first one, it takes much less compute time to calculate
         * a mod and keep track of where the oldest data is.
         *
         * @param value the value to plot
         */
        void addData(float value) {
            value = constrain(value, (float) minValue, (float) maxValue);
            points[dataPos] = value;
            dataPos = (dataPos + 1) % nSamples; // wrap around when array fills

    /*
     * If the array isn't full yet, add to the end of the array
     * Otherwise, the start point for plotting moves through
     * the array.
     */
            if (nPoints < nSamples) {
                nPoints++;
            } else {
                startPos = (startPos + 1) % nSamples;
            }
        }

        void display() {
            int arrayPos;
            float yPos;
            stroke(0);
            fill(255);
            pushMatrix();
            translate(x, y);
            textFont(legendFont);

            // reserve space for the max/min value legend
            rightSpace = Math.max(textWidth(minString), textWidth(maxString));
            rect(0, 0, nSamples + rightSpace + 2 * HSPACE, h + 2 * VSPACE);
            stroke(192);
            line(HSPACE, VSPACE + h / 2, nSamples + rightSpace - HSPACE, VSPACE + h / 2);
            line(nSamples + 1, VSPACE, nSamples + 1, h - VSPACE);

            // draw max and min values
            textFont(legendFont);
            fill(0);
            stroke(0);
            text(minString, nSamples + 2, h - VSPACE);
            text(maxString, nSamples + 2, VSPACE + 8);

            for (int i = 0; i < nPoints; i++) {
                arrayPos = (startPos + i) % nSamples;
                if (period > 0 && arrayPos % period == 0) {
                    stroke(192);
                    line(nSamples - nPoints + i, VSPACE, nSamples - nPoints + i, h - VSPACE);
                }
                stroke(colour);
                yPos = (float) (VSPACE + h * (1.0 - (points[arrayPos] - minValue) / (maxValue - minValue)));

                // Draw a point for the first item, then connect all the other points with lines
                if (i == 0) {
                    point(nSamples - nPoints + i, yPos);
                } else {
                    line(prevX, prevY, nSamples - nPoints + i, yPos);
                }
                prevX = nSamples - nPoints + i;
                prevY = yPos;
            }
            popMatrix();
        }

        /**
         * Add a data value and re-display the strip chart.
         * The addData() and display() methods are decoupled;
         * this lets you "speed up" the chart by adding
         * several points before displaying the chart.
         * This method is a convenience method that does
         * both actions.
         *
         * @param value the value to add and display
         */
        void plot(float value) {
            addData(value);
            display();
        }
    }

}