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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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.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.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 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 JSpinner spinnerDate;
    private Stop departure;

    private TileProvider trTileProvider;
    private ColorTable colorTable;

    private JViewport viewPort;

    /**
     * 
     * @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;
            }
        }

        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);
    }

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

        tiledMapComponent.setSize(tiledMapComponent.getPreferredSize());

        tiledMapComponent.view = viewPort;

        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();
            }
        });

        /**
         * 
         * Méthode appelée lors d'un déplacement de souris
         */
        layeredPane.addMouseMotionListener(new MouseInputAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                mapPoint.x += mousePoint.x - e.getPoint().x;
                mapPoint.y += mousePoint.y - e.getPoint().y;
                mousePoint = e.getPoint();
                viewPort.setViewPosition(mapPoint);
            }
        });

        /**
         * 
         * 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);
            }
        });

        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>();

        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);

        ActionListener listener = new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                setDeparture((Stop) comboStops.getSelectedItem());
            }

        };
        comboStops.addActionListener(listener);

        JSeparator separator = new JSeparator();

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

        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));
        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);

        topPanel.add(departureLabel);
        topPanel.add(comboStops);
        topPanel.add(separator);
        topPanel.add(dateLabel);
        topPanel.add(spinnerDate);

        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
     * 
     */
    private void start() {

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

        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
     * 
     * @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) // si entre
                                                                // minuit et 4 h
                    && 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);
    }

    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();
                }
            }
        });
    }
}
