/*
 * @(#)JTimelineEditor.java  1.0  2011-09-01
 * 
 * Copyright (c) 2011 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 * 
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package org.monte.media.gui;

import org.monte.media.Movie;
import org.monte.media.math.Rational;
import java.awt.Window;
import java.awt.Component;
import javax.swing.ImageIcon;
import javax.swing.Icon;
import org.monte.media.gui.border.ImageBevelBorder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import org.monte.media.image.Images;
import javax.swing.border.Border;
import java.awt.Color;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import javax.swing.SwingUtilities;
import static java.lang.Math.*;

/**
 * JTimelineEditor visualizes the movie timeline, an insertion point and
 * the start and end position of a movie clip.
 * <p>
 * The insertion point (playhead) also shows the current time of the movie.
 * <p>
 * If a movie has n time steps, then there are n+1 insertion points.
 * 
 * @author Werner Randelshofer
 * @version 1.0 2011-09-01 Created.
 */
public class JTimelineEditor extends javax.swing.JPanel {

    private Movie movie;
    /** Track number of the time-base track. Specify -1 for disabling the time
     * track. 
     */
    private int timeTrack = 0;
    private Insets trackInsets = new Insets(6, 10, 6, 10);
    private Dimension inSize = new Dimension(9, 6);
    private Dimension outSize = new Dimension(9, 6);
    private Dimension playheadSize = new Dimension(15, 10);

    enum Handle {

        InsertionPoint, SelectionStart, SelectionEnd;
    }
    private Handle focusedHandle = null;

    private class Handler implements MouseListener, MouseMotionListener, KeyListener, PropertyChangeListener, FocusListener {

        private Handle pressedHandle = null;

        @Override
        public void mouseClicked(MouseEvent e) {
            // do nothing?
        }

        @Override
        public void mousePressed(MouseEvent e) {
            if (movie == null) {
                return;
            }

            Point p = e.getPoint();
            Rectangle phBounds = getInsertionPointBounds();
            Rectangle inBounds = getSelectionStartBounds();
            Rectangle outBounds = getSelectionEndBounds();
            if (phBounds.contains(p)) {
                pressedHandle = focusedHandle = Handle.InsertionPoint;
            } else if (inBounds.contains(p)) {
                pressedHandle = focusedHandle = Handle.SelectionStart;
                movie.setInsertionPoint(movie.getSelectionStart());
            } else if (outBounds.contains(p)) {
                pressedHandle = focusedHandle = Handle.SelectionEnd;
                movie.setInsertionPoint(movie.getSelectionEnd());
            } else {
                int y = e.getY();
                Rational time;
                if (phBounds.contains(e.getX(), phBounds.y)) {
                    // click occured in vertical area belonging to playhead => snap to playhead
                    time = movie.getInsertionPoint();
                } else {
                    time = posToTime(e.getX());
                }

                movie.setInsertionPoint(time);
                if (phBounds.contains(phBounds.x, y)) {
                    // click occured in horizontal area belonging to playhead => move playhead
                    focusedHandle = pressedHandle = Handle.InsertionPoint;
                } else if (inBounds.contains(inBounds.x, y)) {
                    // click occured in horizontal area belonging to in and out point => move in or out point
                    int splitPos = (outBounds.x - inBounds.x - inBounds.width) / 2 + inBounds.x + inBounds.width;
                    if (e.getX() < splitPos) {
                        movie.setSelectionStart(time);
                        movie.setSelectionEnd(Rational.max(movie.getSelectionStart(), movie.getSelectionEnd()));
                        focusedHandle = pressedHandle = Handle.SelectionStart;
                    } else {
                        movie.setSelectionEnd(time);
                        movie.setSelectionStart(Rational.min(movie.getSelectionStart(), movie.getSelectionEnd()));
                        focusedHandle = pressedHandle = Handle.SelectionEnd;
                    }
                }

            }
            if (focusedHandle != null) {
                requestFocus();
            }
            repaint();
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            // do nothing?
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            // do nothing?
        }

        @Override
        public void mouseExited(MouseEvent e) {
            // do nothing?
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (movie == null) {
                return;
            }
            if (pressedHandle != null) {
                Rational time = posToTime(e.getX());
                switch (pressedHandle) {
                    case SelectionStart:
                        time = Rational.min(time, movie.getSelectionEnd());
                        movie.setSelectionStart(time);
                        movie.setInsertionPoint(time);
                        break;
                    case SelectionEnd:
                        time = Rational.max(movie.getSelectionStart(), time);
                        movie.setSelectionEnd(time);
                        movie.setInsertionPoint(time);
                        break;
                    case InsertionPoint:
                        movie.setInsertionPoint(time);
                        break;
                }

            }
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            // do nothing?
        }

        @Override
        public void keyTyped(KeyEvent e) {
            //throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public void keyPressed(KeyEvent e) {
            if (movie == null) {
                return;
            }

            if (focusedHandle != null) {
                Rational time;

                switch (focusedHandle) {
                    case SelectionStart:
                        time = movie.getSelectionStart();
                        break;
                    case SelectionEnd:
                        time = movie.getSelectionEnd();
                        break;
                    case InsertionPoint:
                        time = movie.getInsertionPoint();
                        break;
                    default:
                        return;
                }

                long sample = movie.timeToSample(0, time); // FIXME - Must be video track
                if (e.getKeyCode() == KeyEvent.VK_LEFT) {
                    time = movie.sampleToTime(0, sample - 1);
                } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
                    time = movie.sampleToTime(0, sample + 1);
                }
                switch (focusedHandle) {
                    case SelectionStart:
                        movie.setSelectionStart(time);
                        movie.setSelectionEnd(Rational.max(movie.getSelectionStart(), movie.getSelectionEnd()));
                        movie.setInsertionPoint(time);
                        break;
                    case SelectionEnd:
                        movie.setSelectionEnd(time);
                        movie.setSelectionStart(Rational.min(movie.getSelectionStart(), movie.getSelectionEnd()));
                        movie.setInsertionPoint(time);
                        break;
                    case InsertionPoint:
                        movie.setInsertionPoint(time);
                        break;
                }
            }
        }

        @Override
        public void keyReleased(KeyEvent e) {
            //throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (evt.getPropertyName().equals("style")) {
                updateStyle();
            }
            repaint();
        }

        @Override
        public void focusGained(FocusEvent e) {
            repaint();
        }

        @Override
        public void focusLost(FocusEvent e) {
            repaint();
        }
    }
    private Handler handler = new Handler();

    /** Creates new form JTimelineEditor */
    public JTimelineEditor() {
        initComponents();
        addMouseListener(handler);
        addMouseMotionListener(handler);
        addKeyListener(handler);
        addFocusListener(handler);
        setPreferredSize(new Dimension(200, 22));
        setMinimumSize(new Dimension(100, 22));
        setFocusable(true);
        // putClientProperty("style","textured");
    }

    public Movie getMovie() {
        return movie;
    }

    public void setMovie(Movie newValue) {
        if (this.movie != null) {
            this.movie.removePropertyChangeListener(handler);
        }
        this.movie = newValue;
        if (this.movie != null) {
            this.movie.addPropertyChangeListener(handler);
        }
        repaint();
    }

    @Override
    protected void paintComponent(Graphics gr) {
        Graphics2D g = (Graphics2D) gr;
        super.paintComponent(g);


        boolean isEnabled = isEnabled();
        Window window = SwingUtilities.getWindowAncestor(this);
        boolean isOnActiveWindow = window != null && window.isActive();

        getBackgroundBorder().paintBorder(this, gr, 0, 0, getWidth(), getHeight());


        Rectangle tr = getTrackBounds();
        getTrackBorder(isEnabled && isOnActiveWindow && movie != null).paintBorder(this, gr, tr.x, tr.y, tr.width, tr.height);
        if (movie == null) {
            return;
        }
        int ppos = timeToPos(movie.getInsertionPoint());
        int inpos = timeToPos(movie.getSelectionStart());
        int outpos = timeToPos(movie.getSelectionEnd());
        getThumbBorder(isEnabled && isOnActiveWindow).paintBorder(this, gr, inpos, tr.y, outpos - inpos + 1, tr.height);

        if (isEnabled) {
            boolean isFocused = isFocusOwner();
            getSelectionStartIcon(isEnabled && isFocused && focusedHandle == Handle.SelectionStart).paintIcon(this, gr, inpos, tr.y);
            getSelectionEndIcon(isEnabled && isFocused && focusedHandle == Handle.SelectionEnd).paintIcon(this, gr, outpos, tr.y);
            getInsertionPointIcon(isEnabled && isFocused && focusedHandle == Handle.InsertionPoint).paintIcon(this, gr, ppos, tr.y);
        }
    }

    protected void paintComponentOld(Graphics gr) {
        Graphics2D g = (Graphics2D) gr;
        super.paintComponent(g);
        Rectangle tr = getTrackBounds();
        if (movie != null) {
            int x1 = timeToPos(movie.getSelectionStart());
            int x2 = timeToPos(movie.getSelectionEnd());
            g.setColor(new Color(0xacacac));
            g.fillRect(x1, tr.y + 1, x2 - x1 + 1, tr.height - 1);
        }
        g.setColor(new Color(0x8e8e8e));
        g.drawRect(tr.x, tr.y, tr.width, tr.height);
        if (movie == null) {
            return;
        }

        g.setColor(new Color(0x737373));
        int x = timeToPos(movie.getInsertionPoint());
        g.drawLine(x, tr.y, x, tr.y + tr.height - 1);
        Rectangle r = getInsertionPointBounds();
        Polygon p = new Polygon();
        p.addPoint(r.x, r.y);
        p.addPoint(r.x + r.width - 1, r.y);
        p.addPoint(r.x + r.width / 2, r.y + r.height - 1);
        p.addPoint(r.x, r.y);
        if (focusedHandle == Handle.InsertionPoint && isFocusOwner()) {
            g.setColor(new Color(0x737373));
        } else {
            g.setColor(new Color(0xdddddd));
        }
        g.fill(p);
        g.setColor(new Color(0x737373));
        g.draw(p);

        r = getSelectionStartBounds();
        p = new Polygon();
        p.addPoint(r.x, r.y + r.height - 1);
        p.addPoint(r.x + r.width - 1, r.y + r.height - 1);
        p.addPoint(r.x + r.width - 1, r.y);
        p.addPoint(r.x, r.y + r.height - 1);
        if (focusedHandle == Handle.SelectionStart && isFocusOwner()) {
            g.setColor(new Color(0x737373));
        } else {
            g.setColor(new Color(0xdddddd));
        }
        g.fill(p);
        g.setColor(new Color(0x737373));
        g.draw(p);

        r = getSelectionEndBounds();
        p = new Polygon();
        p.addPoint(r.x, r.y + r.height - 1);
        p.addPoint(r.x + r.width - 1, r.y + r.height - 1);
        p.addPoint(r.x, r.y);
        p.addPoint(r.x, r.y + r.height - 1);
        if (focusedHandle == Handle.SelectionEnd && isFocusOwner()) {
            g.setColor(new Color(0x737373));
        } else {
            g.setColor(new Color(0xdddddd));
        }
        g.fill(p);
        g.setColor(new Color(0x737373));
        g.draw(p);

    }

    protected int timeToPos(Rational time) {
        float fraction = time.divide(movie.getDuration()).floatValue();
        int pos = (int) (fraction * (getWidth() - trackInsets.left - trackInsets.right));
        return pos + trackInsets.left;
    }

    protected Rational posToTime(int pos) {
        Rational fraction = new Rational(pos - trackInsets.left, getWidth() - trackInsets.left - trackInsets.right);
        fraction = Rational.max(new Rational(0, 1), Rational.min(new Rational(1, 1), fraction));

        Rational time = fraction.multiply(movie.getDuration());

        if (timeTrack != -1) {
            long sample = movie.timeToSample(timeTrack, time);
            time = movie.sampleToTime(timeTrack, sample);
        }

        return time;
    }

    protected Rectangle getSelectionStartBounds() {

        int pos = timeToPos(movie.getSelectionStart());
        return new Rectangle(pos - inSize.width, getHeight() - trackInsets.bottom, inSize.width, inSize.height);
    }

    protected Rectangle getSelectionEndBounds() {
        int pos = timeToPos(movie.getSelectionEnd());
        return new Rectangle(pos, getHeight() - trackInsets.bottom, outSize.width, outSize.height);
    }

    protected Rectangle getInsertionPointBounds() {
        int pos = timeToPos(movie.getInsertionPoint());
        return new Rectangle(pos - playheadSize.width / 2, 0, playheadSize.width, playheadSize.height);
    }

    protected Rectangle getTrackBounds() {
        return new Rectangle(trackInsets.left, trackInsets.top, getWidth() - trackInsets.left - trackInsets.right, 10);
    }
    private Border backgroundBorder;

    protected Border getBackgroundBorder() {
        if (backgroundBorder == null) {
            backgroundBorder = readBorders(
                    "images/TimelineEditor.background.png", 1, false, new Insets(3, 3, 3, 3))[0];
        }
        return backgroundBorder;
    }
    private Border[] trackBorders;

    protected Border getTrackBorder(boolean isOnActiveWindow) {
        if (trackBorders == null) {
            trackBorders = readBorders(
                    "images/TimelineEditor.tracks.png", 2, false, new Insets(3, 3, 3, 3));
        }
        return trackBorders[isOnActiveWindow ? 0 : 1];
    }
    private Border[] thumbBorders;

    protected Border getThumbBorder(boolean isOnActiveWindow) {
        if (thumbBorders == null) {
            thumbBorders = readBorders(
                    "images/TimelineEditor.thumbs.png", 2, false, new Insets(3, 3, 3, 3));
        }
        return thumbBorders[isOnActiveWindow ? 0 : 1];
    }
    private Icon[] insertionPointIcon;

    protected Icon getInsertionPointIcon(boolean isFocused) {
        if (insertionPointIcon == null) {
            insertionPointIcon = readIcons("images/TimelineEditor.playHeads.png", 2, false, new Point(-8, -6));
        }
        return insertionPointIcon[isFocused ? 1 : 0];
    }
    private Icon[] selectionStartIcon;

    protected Icon getSelectionStartIcon(boolean isFocused) {
        if (selectionStartIcon == null) {
            selectionStartIcon = readIcons("images/TimelineEditor.inPoints.png", 2, false, new Point(-12, -6));
        }
        return selectionStartIcon[isFocused ? 1 : 0];
    }

    private void updateStyle() {
        trackBorders = null;
        thumbBorders = null;
        insertionPointIcon = null;
        selectionStartIcon = null;
        selectionEndIcon = null;
    }
    private Icon[] selectionEndIcon;

    protected Icon getSelectionEndIcon(boolean isFocused) {
        if (selectionEndIcon == null) {
            selectionEndIcon = readIcons("images/TimelineEditor.outPoints.png", 2, false, new Point(-3, -6));
        }
        return selectionEndIcon[isFocused ? 1 : 0];
    }

    protected Border[] readBorders(String resource, int count, boolean isHorizontal, Insets insets) {
        resource = resource.substring(0, resource.length() - 4) + getStyleSuffix() + ".png";
        try {
            BufferedImage[] imgs = Images.split(ImageIO.read(JTimelineEditor.class.getResource(resource)), count, false);
            Border[] borders = new Border[count];
            for (int i = 0; i < count; i++) {
                borders[i] = new ImageBevelBorder(imgs[i], new Insets(1, 3, 1, 3));
            }
            return borders;
        } catch (Throwable ex) {
            throw new InternalError("JTimelineEditor image not found:" + resource);
        }
    }

    protected Icon[] readIcons(String resource, int count, boolean isHorizontal, final Point offset) {
        resource = resource.substring(0, resource.length() - 4) + getStyleSuffix() + ".png";
        try {
            BufferedImage[] imgs = Images.split(ImageIO.read(JTimelineEditor.class.getResource(resource)), count, false);
            Icon[] icons = new Icon[count];
            for (int i = 0; i < count; i++) {

                icons[i] = new ImageIcon(imgs[i]) {

                    @Override
                    public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
                        super.paintIcon(c, g, x + offset.x, y + offset.y);
                    }
                };
            }
            return icons;
        } catch (Throwable ex) {
            throw new InternalError("JTimelineEditor image not found:" + resource);
        }
    }

    protected String getStyleSuffix() {
        String style = (String) getClientProperty("style");
        return style == null ? "" : "." + style;
    }

    /** Returns the track number used as a time base. If this value is -1,
     * then no track is used as a time base.
     * 
     * @return The track number or -1.
     */
    public int getTimeTrack() {
        return timeTrack;
    }

    /**
     * Sets the track number used as a time base.
     * 
     * @param timeTrack Track number or -1.
     */
    public void setTimeTrack(int timeTrack) {
        this.timeTrack = timeTrack;
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        setLayout(new java.awt.GridLayout(1, 0));
    }// </editor-fold>//GEN-END:initComponents
    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables
}
