/*
 * Copyright 1997 Phil Burk, Mobileer Inc
 *
 * Licensed 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.jsyn.swing;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;

import com.jsyn.data.SegmentedEnvelope;
import com.jsyn.unitgen.VariableRateDataReader;

/**
 * Edit a list of ordered duration,value pairs suitable for use with a SegmentedEnvelope.
 *
 * @author (C) 1997-2013 Phil Burk, SoftSynth.com
 * @see EnvelopePoints
 * @see SegmentedEnvelope
 * @see VariableRateDataReader
 */

/* ========================================================================== */
public class EnvelopeEditorBox extends XYController implements MouseListener, MouseMotionListener {
    EnvelopePoints points;
    ArrayList<EditListener> listeners = new ArrayList<EditListener>();
    int dragIndex = -1;
    double dragLowLimit;
    double dragHighLimit;
    double draggedPoint[];
    double xBefore; // WX value before point
    double xPicked; // WX value of picked point
    double dragWX;
    double dragWY;
    int maxPoints = Integer.MAX_VALUE;
    int radius = 4;
    double verticalBarSpacing = 1.0;
    boolean verticalBarsEnabled = false;
    double maximumXRange = Double.MAX_VALUE;
    double minimumXRange = 0.1;
    int rangeStart = -1; // gx coordinates
    int rangeEnd = -1;
    int mode = EDIT_POINTS;
    public final static int EDIT_POINTS = 0;
    public final static int SELECT_SUSTAIN = 1;
    public final static int SELECT_RELEASE = 2;

    Color rangeColor = Color.RED;
    Color sustainColor = Color.BLUE;
    Color releaseColor = Color.YELLOW;
    Color overlapColor = Color.GREEN;
    Color firstLineColor = Color.GRAY;

    public interface EditListener {
        public void objectEdited(Object editor, Object edited);
    }

    public EnvelopeEditorBox() {
        addMouseListener(this);
        addMouseMotionListener(this);
    }

    public void setMaximumXRange(double maxXRange) {
        maximumXRange = maxXRange;
    }

    public double getMaximumXRange() {
        return maximumXRange;
    }

    public void setMinimumXRange(double minXRange) {
        minimumXRange = minXRange;
    }

    public double getMinimumXRange() {
        return minimumXRange;
    }

    public void setSelection(int start, int end) {
        switch (mode) {
            case SELECT_SUSTAIN:
                points.setSustainLoop(start, end);
                break;
            case SELECT_RELEASE:
                points.setReleaseLoop(start, end);
                break;
        }
        // LOGGER.debug("start = " + start + ", end = " + end );
    }

    /** Set mode to either EDIT_POINTS or SELECT_SUSTAIN, SELECT_RELEASE; */
    public void setMode(int mode) {
        this.mode = mode;
    }

    public int getMode() {
        return mode;
    }

    /**
     * Add a listener to receive edit events. Listener will be passed the editor object and the
     * edited object.
     */
    public void addEditListener(EditListener listener) {
        listeners.add(listener);
    }

    public void removeEditListener(EditListener listener) {
        listeners.remove(listener);
    }

    /** Send event to every subscribed listener. */
    public void fireObjectEdited() {
        for (EditListener listener : listeners) {
            listener.objectEdited(this, points);
        }
    }

    public void setMaxPoints(int maxPoints) {
        this.maxPoints = maxPoints;
    }

    public int getMaxPoints() {
        return maxPoints;
    }

    public int getNumPoints() {
        return points.size();
    }

    public void setPoints(EnvelopePoints points) {
        this.points = points;
        setMaxWorldY(points.getMaximumValue());
    }

    public EnvelopePoints getPoints() {
        return points;
    }

    /**
     * Return index of point before this X position.
     */
    private int findPointBefore(double wx) {
        int pnt = -1;
        double px = 0.0;
        xBefore = 0.0;
        for (int i = 0; i < points.size(); i++) {
            px += points.getDuration(i);
            if (px > wx)
                break;
            pnt = i;
            xBefore = px;
        }
        return pnt;
    }

    private int pickPoint(double wx, double wxAperture, double wy, double wyAperture) {
        double px = 0.0;
        double wxLow = wx - wxAperture;
        double wxHigh = wx + wxAperture;
        // LOGGER.debug("wxLow = " + wxLow + ", wxHigh = " + wxHigh );
        double wyLow = wy - wyAperture;
        double wyHigh = wy + wyAperture;
        // LOGGER.debug("wyLow = " + wyLow + ", wyHigh = " + wyHigh );
        double wxScale = 1.0 / wxAperture; // only divide once, then multiply
        double wyScale = 1.0 / wyAperture;
        int bestPoint = -1;
        double bestDistance = Double.MAX_VALUE;
        for (int i = 0; i < points.size(); i++) {
            double dar[] = points.getPoint(i);
            px += dar[0];
            double py = dar[1];
            // LOGGER.debug("px = " + px + ", py = " + py );
            if ((px > wxLow) && (px < wxHigh) && (py > wyLow) && (py < wyHigh)) {
                /* Inside pick range. Calculate distance squared. */
                double ndx = (px - wx) * wxScale;
                double ndy = (py - wy) * wyScale;
                double dist = (ndx * ndx) + (ndy * ndy);
                // LOGGER.debug("dist = " + dist );
                if (dist < bestDistance) {
                    bestPoint = i;
                    bestDistance = dist;
                    xPicked = px;
                }
            }
        }
        return bestPoint;
    }

    private void clickDownRange(boolean shiftDown, int gx, int gy) {
        setSelection(-1, -1);
        rangeStart = rangeEnd = gx;
        repaint();
    }

    private void dragRange(int gx, int gy) {
        rangeEnd = gx;
        repaint();
    }

    private void clickUpRange(int gx, int gy) {
        dragRange(gx, gy);
        if (rangeEnd < rangeStart) {
            int temp = rangeEnd;
            rangeEnd = rangeStart;
            rangeStart = temp;
        }
        // LOGGER.debug("clickUpRange: gx = " + gx + ", rangeStart = " +
        // rangeStart );
        double wx = convertGXtoWX(rangeStart);
        int i0 = findPointBefore(wx);
        wx = convertGXtoWX(rangeEnd);
        int i1 = findPointBefore(wx);

        if (i1 == i0) {
            // set single point at zero so there is nothing played for queueOn()
            if (gx < 0) {
                setSelection(0, 0);
            }
            // else clear any existing loop
        } else if (i1 == (i0 + 1)) {
            setSelection(i1 + 1, i1 + 1); // set to a single point
        } else if (i1 > (i0 + 1)) {
            setSelection(i0 + 1, i1 + 1); // set to a range of two or more
        }

        rangeStart = -1;
        rangeEnd = -1;
        fireObjectEdited();
    }

    private void clickDownPoints(boolean shiftDown, int gx, int gy) {
        dragIndex = -1;
        double wx = convertGXtoWX(gx);
        double wy = convertGYtoWY(gy);
        // calculate world values for aperture
        double wxAp = convertGXtoWX(radius + 2) - convertGXtoWX(0);
        // LOGGER.debug("wxAp = " + wxAp );
        double wyAp = convertGYtoWY(0) - convertGYtoWY(radius + 2);
        // LOGGER.debug("wyAp = " + wyAp );
        int pnt = pickPoint(wx, wxAp, wy, wyAp);
        // LOGGER.debug("pickPoint = " + pnt);
        if (shiftDown) {
            if (pnt >= 0) {
                points.removePoint(pnt);
                repaint();
            }
        } else {
            if (pnt < 0) // didn't hit one so look for point to left of click
            {
                if (points.size() < maxPoints) // add if room
                {
                    pnt = findPointBefore(wx);
                    // LOGGER.debug("pointBefore = " + pnt);
                    dragIndex = pnt + 1;
                    if (pnt == (points.size() - 1)) {
                        points.add(wx - xBefore, wy);
                    } else {
                        points.insert(dragIndex, wx - xBefore, wy);
                    }
                    dragLowLimit = xBefore;
                    dragHighLimit = wx + (maximumXRange - points.getTotalDuration());
                    repaint();
                }
            } else
            // hit one so drag it
            {
                dragIndex = pnt;
                if (dragIndex <= 0)
                    dragLowLimit = 0.0; // FIXME envelope drag limit
                else
                    dragLowLimit = xPicked - points.getPoint(dragIndex)[0];
                dragHighLimit = xPicked + (maximumXRange - points.getTotalDuration());
                // LOGGER.debug("dragLowLimit = " + dragLowLimit );
            }
        }
        // Set up drag point if we are dragging.
        if (dragIndex >= 0) {
            draggedPoint = points.getPoint(dragIndex);
        }

    }

    private void dragPoint(int gx, int gy) {
        if (dragIndex < 0)
            return;

        double wx = convertGXtoWX(gx);
        if (wx < dragLowLimit)
            wx = dragLowLimit;
        else if (wx > dragHighLimit)
            wx = dragHighLimit;
        draggedPoint[0] = wx - dragLowLimit; // duration

        double wy = convertGYtoWY(gy);
        wy = clipWorldY(wy);
        draggedPoint[1] = wy;
        dragWY = wy;
        dragWX = wx;
        points.setDirty(true);
        repaint();
    }

    private void clickUpPoints(int gx, int gy) {
        dragPoint(gx, gy);
        fireObjectEdited();
        dragIndex = -1;
    }

    // Implement the MouseMotionListener interface for AWT 1.1
    @Override
    public void mouseDragged(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();
        if (points == null)
            return;
        if (mode == EDIT_POINTS) {
            dragPoint(x, y);
        } else {
            dragRange(x, y);
        }
    }

    @Override
    public void mouseMoved(MouseEvent e) {
    }

    // Implement the MouseListener interface for AWT 1.1
    @Override
    public void mousePressed(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();
        if (points == null)
            return;
        if (mode == EDIT_POINTS) {
            clickDownPoints(e.isShiftDown(), x, y);
        } else {
            clickDownRange(e.isShiftDown(), x, y);
        }
    }

    @Override
    public void mouseClicked(MouseEvent e) {
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();
        if (points == null)
            return;
        if (mode == EDIT_POINTS) {
            clickUpPoints(x, y);
        } else {
            clickUpRange(x, y);
        }
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }

    /**
     * Draw selected range.
     */
    private void drawRange(Graphics g) {
        if (rangeStart >= 0) {
            int height = getHeight();
            int gx0 = 0, gx1 = 0;

            if (rangeEnd < rangeStart) {
                gx0 = rangeEnd;
                gx1 = rangeStart;
            } else {
                gx0 = rangeStart;
                gx1 = rangeEnd;
            }
            g.setColor(rangeColor);
            g.fillRect(gx0, 0, gx1 - gx0, height);
        }
    }

    private void drawUnderSelection(Graphics g, int start, int end) {
        if (start >= 0) {
            int height = getHeight();
            int gx0 = 0, gx1 = radius;
            double wx = 0.0;
            for (int i = 0; i <= (end - 1); i++) {
                double dar[] = (double[]) points.elementAt(i);
                wx += dar[0];
                if (start == (i + 1)) {
                    gx0 = convertWXtoGX(wx) + radius;
                }
                if (end == (i + 1)) {
                    gx1 = convertWXtoGX(wx) + radius;
                }
            }
            if (gx0 == gx1)
                gx0 = gx0 - radius;
            g.fillRect(gx0, 0, gx1 - gx0, height);
        }
    }

    private void drawSelections(Graphics g) {
        int sus0 = points.getSustainBegin();
        int sus1 = points.getSustainEnd();
        int rel0 = points.getReleaseBegin();
        int rel1 = points.getReleaseEnd();

        g.setColor(sustainColor);
        drawUnderSelection(g, sus0, sus1);
        g.setColor(releaseColor);
        drawUnderSelection(g, rel0, rel1);
        // draw overlapping sustain and release region
        if (sus1 >= rel0) {
            int sel1 = (rel1 < sus1) ? rel1 : sus1;
            g.setColor(overlapColor);
            drawUnderSelection(g, rel0, sel1);
        }
    }

    /**
     * Override this to draw a grid or other stuff under the envelope.
     */
    public void drawUnderlay(Graphics g) {
        if (dragIndex < 0) {
            drawSelections(g);
            drawRange(g);
        }
        if (verticalBarsEnabled)
            drawVerticalBars(g);
    }

    public void setVerticalBarsEnabled(boolean flag) {
        verticalBarsEnabled = flag;
    }

    public boolean areVerticalBarsEnabled() {
        return verticalBarsEnabled;
    }

    /**
     * Set spacing in world coordinates.
     */
    public void setVerticalBarSpacing(double spacing) {
        verticalBarSpacing = spacing;
    }

    public double getVerticalBarSpacing() {
        return verticalBarSpacing;
    }

    /**
     * Draw vertical lines.
     */
    private void drawVerticalBars(Graphics g) {
        int width = getWidth();
        int height = getHeight();
        double wx = verticalBarSpacing;
        int gx;

        // g.setColor( getBackground().darker() );
        g.setColor(Color.lightGray);
        while (true) {
            gx = convertWXtoGX(wx);
            if (gx > width)
                break;
            g.drawLine(gx, 0, gx, height);
            wx += verticalBarSpacing;
        }
    }

    public void drawPoints(Graphics g, Color lineColor) {
        double wx = 0.0;
        int gx1 = 0;
        int gy1 = getHeight();
        for (int i = 0; i < points.size(); i++) {
            double dar[] = (double[]) points.elementAt(i);
            wx += dar[0];
            double wy = dar[1];
            int gx2 = convertWXtoGX(wx);
            int gy2 = convertWYtoGY(wy);
            if (i == 0) {
                g.setColor(isEnabled() ? firstLineColor : firstLineColor.darker());
                g.drawLine(gx1, gy1, gx2, gy2);
                g.setColor(isEnabled() ? lineColor : lineColor.darker());
            } else if (i > 0) {
                g.drawLine(gx1, gy1, gx2, gy2);
            }
            int diameter = (2 * radius) + 1;
            g.fillOval(gx2 - radius, gy2 - radius, diameter, diameter);
            gx1 = gx2;
            gy1 = gy2;
        }
    }

    public void drawAllPoints(Graphics g) {
        drawPoints(g, getForeground());
    }

    /* Override default paint action. */
    @Override
    public void paint(Graphics g) {
        double wx = 0.0;
        int width = getWidth();
        int height = getHeight();

        // draw background and erase all values
        g.setColor(isEnabled() ? getBackground() : getBackground().darker());
        g.fillRect(0, 0, width, height);

        if (points == null) {
            g.setColor(getForeground());
            g.drawString("No EnvelopePoints", 10, 30);
            return;
        }

        // Determine total duration.
        if (points.size() > 0) {
            wx = points.getTotalDuration();
            // Adjust max X so that we see entire circle of last point.
            double radiusWX = this.convertGXtoWX(radius) - this.getMinWorldX();
            double wxFar = wx + radiusWX;
            if (wxFar > getMaxWorldX()) {
                if (wx > maximumXRange)
                    wxFar = maximumXRange;
                setMaxWorldX(wxFar);
            } else if (wx < (getMaxWorldX() * 0.7)) {
                double newMax = wx / 0.7001; // make slightly larger to prevent
                                             // endless jitter, FIXME - still
                                             // needed after repaint()
                                             // removed from setMaxWorldX?
                // LOGGER.debug("newMax = " + newMax );
                if (newMax < minimumXRange)
                    newMax = minimumXRange;
                setMaxWorldX(newMax);
            }
        }
        // LOGGER.debug("total X = " + wx );

        drawUnderlay(g);

        drawAllPoints(g);

        /* Show X,Y,TotalX as text. */
        g.drawString(points.getName() + ", len=" + String.format("%7.3f", wx), 5, 15);
        if ((draggedPoint != null) && (dragIndex >= 0)) {
            String s = "i=" + dragIndex + ", dur="
                    + String.format("%7.3f", draggedPoint[0]) + ", y = "
                    + String.format("%8.4f", draggedPoint[1]);
            g.drawString(s, 5, 30);
        }
    }
}
