package ch.epfl.isochrone.gui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.Vector;

import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.JViewport;
import javax.swing.SpinnerDateModel;
import javax.swing.SwingConstants;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.MouseInputAdapter;

import ch.epfl.isochrone.geo.PointOSM;
import ch.epfl.isochrone.geo.PointWGS84;
import ch.epfl.isochrone.tiledmap.CachedTileProvider;
import ch.epfl.isochrone.tiledmap.ColorTable;
import ch.epfl.isochrone.tiledmap.IsochroneTileProvider;
import ch.epfl.isochrone.tiledmap.OSMTileProvider;
import ch.epfl.isochrone.tiledmap.PathTileProvider;
import ch.epfl.isochrone.tiledmap.TileProvider;
import ch.epfl.isochrone.tiledmap.TransparentTileProvider;
import ch.epfl.isochrone.timetable.Date;
import ch.epfl.isochrone.timetable.Date.Month;
import ch.epfl.isochrone.timetable.FastestPathTree;
import ch.epfl.isochrone.timetable.Graph;
import ch.epfl.isochrone.timetable.SecondsPastMidnight;
import ch.epfl.isochrone.timetable.Service;
import ch.epfl.isochrone.timetable.Stop;
import ch.epfl.isochrone.timetable.TimeTable;
import ch.epfl.isochrone.timetable.TimeTableReader;

/**
 * La classe principale du projet, avec gestion de l'interface graphique
 * 
 * @author Charles Thiebaut (238935)
 * @author Arnaud Delamare (234841)
 * 
 */
public final class IsochroneTL {
    private static final String OSM_TILE_URL = "http://b.tile.openstreetmap.org/";
    private static final int INITIAL_ZOOM = 11;
    private static final PointWGS84 INITIAL_POSITION = new PointWGS84(
            Math.toRadians(6.476), Math.toRadians(46.613));
    private static final String INITIAL_STARTING_STOP_NAME = "Lausanne-Flon";
    private static final int INITIAL_DEPARTURE_TIME = SecondsPastMidnight
            .fromHMS(6, 8, 0);
    private static final Date INITIAL_DATE = new Date(1, Month.OCTOBER, 2013);
    private static final int WALKING_TIME = 5 * 60;
    private static final double WALKING_SPEED = 1.25;
    private static final int IMMOBILISATION_TIME = 500;
    private static final int ANIMATION_TIME = 1;

    private final TiledMapComponent tiledMapComponent;

    private Point mousePoint;
    private Point mapPoint = new Point(0, 0);
    private TimeTable timeTable;
    private TimeTableReader timeTableReader;
    private Set<Service> services;
    private Graph graph;
    private Date date = INITIAL_DATE;
    private int hour = INITIAL_DEPARTURE_TIME;
    private FastestPathTree tree;

    private JComboBox<Stop> comboStops;
    private JComboBox<String> comboOption; 
    private JSpinner spinnerDate;
    private Stop departure;
    
    private java.util.Date dateBeforeAnimation;
    private Stop stopBeforeAnimation;

    private TileProvider trTileProvider;
    private TileProvider pathTileProvider;
    private ColorTable colorTable;

    private JViewport viewPort;
    private Timer timer;
    private Timer timerAnimation;
    /**
     * 
     * @throws IOException
     *             lorsqu'il y a une erreur de lecture de fichier
     */
    public IsochroneTL() throws IOException {
        TileProvider bgTileProvider = new CachedTileProvider(
                new OSMTileProvider(new URL(OSM_TILE_URL)));
        tiledMapComponent = new TiledMapComponent(INITIAL_ZOOM);

        timeTableReader = new TimeTableReader("/time-table/");
        timeTable = timeTableReader.readTimeTable();

        departure = null;
        for (Stop s : timeTable.stops()) {
            if (s.name().equals(INITIAL_STARTING_STOP_NAME)) {
                departure = s;
            }
        }
        stopBeforeAnimation = departure; 

        services = timeTable.servicesForDate(INITIAL_DATE);
        graph = timeTableReader.readGraphForServices(timeTable.stops(),
                services, WALKING_TIME, WALKING_SPEED);

        tree = graph.fastestPaths(departure, INITIAL_DEPARTURE_TIME);

        List<Color> colorList = new ArrayList<Color>();
        colorList.add(new Color(255, 0, 0));
        colorList.add(new Color(255, 122, 0));
        colorList.add(new Color(255, 255, 0));
        colorList.add(new Color(122, 255, 0));
        colorList.add(new Color(0, 255, 0));
        colorList.add(new Color(0, 122, 122));
        colorList.add(new Color(0, 0, 255));
        colorList.add(new Color(0, 0, 122));
        colorList.add(new Color(0, 0, 0));
        colorTable = new ColorTable(300, colorList);

        trTileProvider = new CachedTileProvider(
                new TransparentTileProvider(0.5, new IsochroneTileProvider(
                        tree, colorTable, WALKING_SPEED)));

        tiledMapComponent.addTileProvider(bgTileProvider);
        tiledMapComponent.addTileProvider(trTileProvider);
        
        timer = new Timer(IMMOBILISATION_TIME, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                updateArrival();
            }
        });
        
        timerAnimation = new Timer(ANIMATION_TIME, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                updateAnimation();
            }
        });
    }

    /**
     * 
     * @return JComponent Le composant qui correspond au centre de la fenêtre
     */
    @SuppressWarnings("deprecation")
    private JComponent createCenterPanel() {
        viewPort = new JViewport();
        viewPort.setView(tiledMapComponent);
        PointOSM startingPosOSM = INITIAL_POSITION.toOSM(tiledMapComponent
                .zoom());
        viewPort.setViewPosition(new Point(startingPosOSM.roundedX(),
                startingPosOSM.roundedY()));
        
        mapPoint = viewPort.getViewPosition();

        tiledMapComponent.setSize(tiledMapComponent.getPreferredSize());
        
        final JPanel copyrightPanel = createCopyrightPanel();

        final JLayeredPane layeredPane = new JLayeredPane();
        layeredPane.setPreferredSize(new Dimension(400, 300));
        
        

        layeredPane.add(viewPort, new Integer(0));
        layeredPane.add(copyrightPanel, new Integer(1));

        layeredPane.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                Rectangle newBounds = layeredPane.getBounds();

                viewPort.setBounds(newBounds);
                copyrightPanel.setBounds(newBounds);

                viewPort.revalidate();
                copyrightPanel.revalidate();
            }
        });

        /**
         * 
         * Méthode appelée lors d'un clic de souris
         */
        layeredPane.addMouseListener(new MouseInputAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                mousePoint = e.getPoint();
                mapPoint = viewPort.getViewPosition();
                if (e.getButton()==MouseEvent.BUTTON3)  // clic droit
                {
                    double distanceMin = Double.MAX_VALUE;
                    Stop closestToDeparture = null;
                    PointWGS84 wgs = new PointOSM(tiledMapComponent.zoom(), mapPoint.x+mousePoint.x, mapPoint.y+mousePoint.y).toWGS84();
                    for (Stop s : timeTable.stops())
                    {
                        double d = s.position().distanceTo(wgs);
                        if (d<distanceMin)
                        {
                            distanceMin=d;
                            closestToDeparture = s;
                        }
                    }
                    
                    if (distanceMin<Double.MAX_VALUE)
                    {
                        setDeparture(closestToDeparture);
                        comboStops.setSelectedItem(closestToDeparture);
                    }
                }
            }
        });

        /**
         * 
         * Méthode appelée lors d'un déplacement de souris
         */
        layeredPane.addMouseMotionListener(new MouseInputAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                if (e.getButton()==MouseEvent.BUTTON1)
                {
                    mapPoint.x += mousePoint.x - e.getPoint().x;
                    mapPoint.y += mousePoint.y - e.getPoint().y;
                    mousePoint = e.getPoint();
                    viewPort.setViewPosition(mapPoint);
                }
            }
            
            @Override
            public void mouseMoved(MouseEvent e) {
                mapPoint = viewPort.getViewPosition();
                tiledMapComponent.removeTileProvider(2); // le PathTileProvider
                mousePoint = e.getPoint();
                timer.restart();
                
            }
        });

        /**
         * 
         * Méthode appelée lors d'un déplacement de la molette de la souris
         */
        layeredPane.addMouseWheelListener(new MouseInputAdapter() {
            @Override
            public void mouseWheelMoved(MouseWheelEvent e) {
                setZoom(tiledMapComponent.zoom() - e.getWheelRotation(),
                        e.getPoint().x, e.getPoint().y);
            }
        });

        layeredPane.setFocusable(true);
        layeredPane.requestFocus();
        layeredPane.requestDefaultFocus();
        layeredPane.requestFocusInWindow();                                // pour mettre le focus sur le
                                        // centerPanel, pour la gestion du
                                        // clavier

        layeredPane.addKeyListener(new KeyListener() {

            @Override
            public void keyTyped(KeyEvent e) {
                // TODO Auto-generated method stub

            }

            @Override
            public void keyPressed(KeyEvent e) {

                mapPoint = viewPort.getViewPosition();
                int c = e.getKeyCode();
                if (c == KeyEvent.VK_UP || c == 87) { // flèche haut ou W pour
                                                      // se dépalcer vers le
                                                      // haut
                    mapPoint.y -= 10;
                } else if (c == KeyEvent.VK_DOWN || c == 83) { // flèche bas ou
                                                               // S pour se
                                                               // déplacer vers
                                                               // le bas
                    mapPoint.y += 10;
                } else if (c == KeyEvent.VK_LEFT || c == 65) { // flèche gauche
                                                               // ou A pour se
                                                               // déplacer vers
                                  System.out.println("Kapoue");                             // la gauche
                    mapPoint.x -= 10;
                } else if (c == KeyEvent.VK_RIGHT || c == 68) { // flèche droite
                                                                // ou D pout se
                                                                // déplacer vers
                                                                // la droite
                    mapPoint.x += 10;
                }

                if (c == 81 || c == 69) // Q ou E pour zoomer au dezoomer
                {
                    int newZoom = 0;
                    if (c == 81) {
                        newZoom = tiledMapComponent.zoom() - 1;
                    }
                    if (c == 69) {
                        newZoom = tiledMapComponent.zoom() + 1;
                    }

                    setZoom(newZoom, viewPort.getWidth() / 2,
                            viewPort.getHeight() / 2);

                }

            }

            @Override
            public void keyReleased(KeyEvent arg0) {
                // TODO Auto-generated method stub

            }

        });

        JPanel centerPanel = new JPanel(new BorderLayout());
       
        centerPanel.add(layeredPane, BorderLayout.CENTER);

        return centerPanel;
    }

    /**
     * 
     * @return JComponent Le composant qui correspond à la barre du haut de la
     *         fenêtre
     */
    @SuppressWarnings("deprecation")
    private JComponent createTopPanel() {

        JPanel topPanel = new JPanel(new FlowLayout());

      
        
        JLabel departureLabel = new JLabel("Depart :");

        Vector<Stop> stopsVector = new Vector<Stop>();
        
        Vector<String> OptionVector = new Vector<String>();
        
        OptionVector.add("Sans Animation");
        OptionVector.add("Heure par Heure");
        OptionVector.add("Minute par Minute");
        OptionVector.add("Arrêt Aléatoire");

        for (Stop s : timeTable.stops()) {
            stopsVector.add(s);

            if (s.name().equals(INITIAL_STARTING_STOP_NAME))
                departure = s;
        }

        Collections.sort(stopsVector);

        comboStops = new JComboBox<Stop>(stopsVector);
        comboStops.setSelectedItem(departure);
        
        comboOption = new JComboBox<String>(OptionVector);
        comboOption.setSelectedItem("Sans Animation");

        ActionListener listener = new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                setDeparture((Stop) comboStops.getSelectedItem());
                if(comboOption.getSelectedItem().equals("Sans Animation")){
                    stopBeforeAnimation = (Stop) comboStops.getSelectedItem();
                    System.out.println(stopBeforeAnimation.name());
                }
                
            }

        };
        comboStops.addActionListener(listener);

        JSeparator separator1 = new JSeparator();

        JLabel dateLabel = new JLabel("Date et Heure");

        final SpinnerDateModel model = new SpinnerDateModel();
        java.util.Date utilDate = INITIAL_DATE.toJavaDate();
        utilDate.setHours(SecondsPastMidnight.hours(INITIAL_DEPARTURE_TIME));
        utilDate.setMinutes(SecondsPastMidnight.minutes(INITIAL_DEPARTURE_TIME));
        utilDate.setSeconds(SecondsPastMidnight.seconds(INITIAL_DEPARTURE_TIME));
        dateBeforeAnimation = utilDate;
        model.setValue(utilDate);

        spinnerDate = new JSpinner(model);
        ChangeListener cl = new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {
                setDateAndHour((java.util.Date) spinnerDate.getModel()
                        .getValue());
            }
        };
        model.addChangeListener(cl);
        
        JSeparator separator2 = new JSeparator();
           
        
        ActionListener listener1 = new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if(!comboOption.getSelectedItem().equals("Sans Animation")){
                    timerAnimation.start();
                }
                else{
                    model.setValue(dateBeforeAnimation);
                    comboStops.setSelectedItem(stopBeforeAnimation);
                    setDeparture(stopBeforeAnimation);
                    timerAnimation.stop();
                }
               
            }

        };
        
        comboOption.addActionListener(listener1);

       
        topPanel.add(separator1);
        topPanel.add(departureLabel);
        topPanel.add(comboStops);
        topPanel.add(separator1);
        topPanel.add(dateLabel);
        topPanel.add(spinnerDate);
        topPanel.add(separator2);
        topPanel.add(comboOption);
        

        return topPanel;
    }

    /**
     * 
     * @return JPanel Le panneau qui correspond au copyright en bas
     */
    private JPanel createCopyrightPanel() {
        Icon tlIcon = new ImageIcon(getClass().getResource(
                "/images/tl-logo.png"));
        String copyrightText = "Données horaires 2013. Source : Transports publics de la région lausannoise / Carte : © contributeurs d'OpenStreetMap";
        JLabel copyrightLabel = new JLabel(copyrightText, tlIcon,
                SwingConstants.CENTER);
        copyrightLabel.setOpaque(true);
        copyrightLabel.setForeground(new Color(1f, 1f, 1f, 0.6f));
        copyrightLabel.setBackground(new Color(0f, 0f, 0f, 0.4f));
        copyrightLabel.setBorder(BorderFactory.createEmptyBorder(3, 0, 5, 0));

        JPanel copyrightPanel = new JPanel(new BorderLayout());
        copyrightPanel.add(copyrightLabel, BorderLayout.PAGE_END);
        copyrightPanel.setOpaque(false);
        return copyrightPanel;
    }

    /**
     * méthode qui lance la fenêtre, avec la possibilité de mettre en
     * plein-écran sous OS-X
     * 
     */
    private void start() {

        // full Sceen MAC OS X
        if (System.getProperty("os.name").indexOf("Mac OS X") >= 0) {
            System.setProperty(
                    "com.apple.mrj.application.apple.menu.about.name",
                    "Isochrome TL");

        }

        JFrame frame = new JFrame("Isochrone TL");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // full Sceen MAC OS X
        if (System.getProperty("os.name").indexOf("Mac OS X") >= 0) {
            String appleFull = "com.apple.eawt.FullScreenUtilities";
            String CanFullSceen = "setWindowCanFullScreen";

            try {
                Class<?> claz = Class.forName(appleFull);
                Method method = claz.getMethod(CanFullSceen, new Class<?>[] {
                        Window.class, boolean.class });
                method.invoke(null, frame, true);
            } catch (Throwable te) {
                te.printStackTrace();
            }

        }

        frame.getContentPane().setLayout(new BorderLayout());
        frame.getContentPane().add(createTopPanel(), BorderLayout.PAGE_START);
        frame.getContentPane().add(createCenterPanel(), BorderLayout.CENTER);

        frame.pack();

        frame.setVisible(true);
    }

    /**
     * 
     * Méthode servant à changer le niveau de zoom de l'application, appelé soit
     * par le déplacement de la molette, soit par la pression de 'Q' ou 'E'
     * 
     * @param newZoom
     *            Le nouveau zoom
     */
    private void setZoom(int newZoom, int newX, int newY) {

        if (newZoom < 10)
            newZoom = 10;

        if (newZoom > 19)
            newZoom = 19;

        mapPoint = viewPort.getViewPosition();

        mapPoint.x += newX;
        mapPoint.y += newY;

        mapPoint.x = (int) (mapPoint.x * Math.pow(2, newZoom
                - tiledMapComponent.zoom()));
        mapPoint.y = (int) (mapPoint.y * Math.pow(2, newZoom
                - tiledMapComponent.zoom()));

        mapPoint.x -= newX;
        mapPoint.y -= newY;

        viewPort.setViewPosition(mapPoint);
        tiledMapComponent.setZoom(newZoom);
        tiledMapComponent.repaint();

        viewPort.setViewPosition(mapPoint);

        tiledMapComponent.repaint();
    }

    /**
     * 
     * Change la date et l'heure pour la carte isochrone
     * 
     * @param utilDate
     *            La date au format java.util.Date
     */
    private void setDateAndHour(java.util.Date utilDate) {
        if (!utilDate.equals(date.toJavaDate())) {
            int newHour = SecondsPastMidnight.fromJavaDate(utilDate);

            Date newDate = new Date(utilDate);

            if (newHour >= SecondsPastMidnight.fromHMS(0, 0, 0)
                    && newHour <= SecondsPastMidnight.fromHMS(4, 0, 0)) {
                newHour += SecondsPastMidnight.fromHMS(24, 0, 0);
                newDate = newDate.relative(-1);
            }

            if (!newDate.equals(date)) {
                date = newDate;
                updateServicesAndGraph();
                updateTreeAndIsochrone();
            } else {
                if (newHour != hour) {
                    hour = newHour;
                    updateTreeAndIsochrone();
                }
            }

        }
    }
    
    /**
     * 
     * @param s
     *        Le nouvel arret de depart
     */
    private void setDeparture(Stop s) {
        if (!s.equals(departure)) {
            departure = s;
            updateTreeAndIsochrone();
            
        }
    }
    
    /**
     * 
     * Update le set des services et le graph
     */
    private void updateServicesAndGraph() {
        Set<Service> newServices = timeTable.servicesForDate(date);

        if (!newServices.equals(services)) {
            services = newServices;

            try {
                graph = timeTableReader.readGraphForServices(timeTable.stops(),
                        services, WALKING_TIME, WALKING_SPEED);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    
    /**
     * 
     * Update le fastestPathTree et la carte isochrone
     */
    private void updateTreeAndIsochrone() {
        tree = graph.fastestPaths(departure, hour);
        trTileProvider = new CachedTileProvider(
                new TransparentTileProvider(0.5, new IsochroneTileProvider(
                        tree, colorTable, WALKING_SPEED)));

        tiledMapComponent.replaceTileProvider(1, trTileProvider);
        tiledMapComponent.removeTileProvider(2);
        
    }
    
    /**
     * 
     * Update l'endroit d'arrivé placé sous la souris
     */
    private void updateArrival() {
        timer.stop();
        int minTime=Integer.MAX_VALUE;
        Stop closest=null;
        PointWGS84 arrival = new PointOSM(tiledMapComponent.zoom(), mapPoint.x+mousePoint.x, mapPoint.y+mousePoint.y).toWGS84();
        
        for (Stop s : tree.stops())
        {
            int time = tree.arrivalTime(s) + (int)(arrival.distanceTo(s.position())/WALKING_SPEED);
            if (time < minTime)
            {
                minTime = time; 
                closest = s;
            }
        }
        
        if (minTime!=Integer.MAX_VALUE)
        {
            tiledMapComponent.removeTileProvider(2);
            pathTileProvider = new CachedTileProvider(
                    new PathTileProvider(tree.pathTo(closest), arrival, tree.arrivalTime(closest)));

            tiledMapComponent.addTileProvider(pathTileProvider);
        }
    }
    
    /**
     * 
     * Update l'endroit d'arrivé placé sous la souris
     */
    @SuppressWarnings("deprecation")
    private void updateAnimation() {
       
        Random r = new Random(); 
        Date newDate =  new Date(date.day(), date.month(), date.year());;
        int newHour = hour;
        
        switch ((String)comboOption.getSelectedItem()){
        
        case "Minute par Minute":
            newHour = hour + SecondsPastMidnight.fromHMS(0, 1, 0);
            break; 
            
        case "Heure par Heure":
            newHour = hour + SecondsPastMidnight.fromHMS(1, 0, 0);
            break;
            
        case "Arrêt Aléatoire":
           
            List<Stop> stops = new ArrayList<Stop>(timeTable.stops());
            
            int index = r.nextInt((int)stops.size());
            Stop newStop = stops.get(index);
         
            comboStops.setSelectedItem(newStop);
            break; 
        
        }
    
              
        if (newHour<0)
        {
            newDate = newDate.relative(-1);
            newHour+=SecondsPastMidnight.fromHMS(24, 0, 0);
        }
        
        java.util.Date utilDate = newDate.toJavaDate();
        utilDate.setHours(SecondsPastMidnight.hours(newHour));
        utilDate.setMinutes(SecondsPastMidnight.minutes(newHour));
        utilDate.setSeconds(SecondsPastMidnight.seconds(newHour));
        
        
        spinnerDate.getModel().setValue(utilDate);
        setDateAndHour(utilDate);
        
    }

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    new IsochroneTL().start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}