package edu.sc.seis.gee.task;

import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import javax.swing.Box;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.DateFormatter;

import net.sf.nachocalendar.CalendarFactory;
import net.sf.nachocalendar.components.CalendarUtils;
import net.sf.nachocalendar.components.DateField;

import org.apache.log4j.Category;

import edu.iris.Fissures.Area;
import edu.iris.Fissures.Plottable;
import edu.iris.Fissures.Quantity;
import edu.iris.Fissures.Time;
import edu.iris.Fissures.TimeRange;
import edu.iris.Fissures.IfEvent.EventAccessOperations;
import edu.iris.Fissures.IfNetwork.Channel;
import edu.iris.Fissures.IfNetwork.ChannelId;
import edu.iris.Fissures.IfNetwork.Station;
import edu.iris.Fissures.model.GlobalAreaImpl;
import edu.iris.Fissures.model.MicroSecondDate;
import edu.iris.Fissures.model.QuantityImpl;
import edu.iris.Fissures.model.UnitImpl;
import edu.iris.Fissures.network.ChannelImpl;
import edu.iris.Fissures.network.StationImpl;
import edu.sc.seis.TauP.Arrival;
import edu.sc.seis.fissuresUtil.cache.AbstractJob;
import edu.sc.seis.fissuresUtil.cache.JobTracker;
import edu.sc.seis.fissuresUtil.cache.WorkerThreadPool;
import edu.sc.seis.fissuresUtil.chooser.ChannelChooser;
import edu.sc.seis.fissuresUtil.chooser.ChannelChooserException;
import edu.sc.seis.fissuresUtil.chooser.ClockUtil;
import edu.sc.seis.fissuresUtil.chooser.StationSelectionEvent;
import edu.sc.seis.fissuresUtil.chooser.StationSelectionListener;
import edu.sc.seis.fissuresUtil.display.DisplayUtils;
import edu.sc.seis.fissuresUtil.display.PlottableDisplay;
import edu.sc.seis.fissuresUtil.exceptionHandler.GlobalExceptionHandler;
import edu.sc.seis.gee.CommonAccess;
import edu.sc.seis.gee.NetworkGateKeeper;
import edu.sc.seis.gee.NoNetworkException;
import edu.sc.seis.gee.configurator.ConfigurationException;
import edu.sc.seis.seisFile.SeisFileException;

/**
 * PlottableTask.java
 * 
 */
public class PlottableTask extends JPanel implements Task {

    public void configure(Map params) throws ConfigurationException {
        setLayout(new BorderLayout());
        this.configParams = params;
        if(params.containsKey("Placeholder")) {
            String placeholderID = (String)params.get("Placeholder");
            TaskAction placeholderTA = CommonAccess.getCommonAccess()
                    .getTaskAction(placeholderID);
            placeholder = (PlaceholderImage)placeholderTA.getTask();
        }
        CommonAccess commonAccess = CommonAccess.getCommonAccess();
        String taskId = (String)configParams.get("queryEventsTask");
        TaskAction taskAction = commonAccess.getTaskAction(taskId);
        queryEventsTask = (QueryEventsTask)taskAction.getTask();
        host = (String)configParams.get("host");
        if (configParams.containsKey("port")) {
            port = Integer.parseInt((String)configParams.get("port"));
        }
        taskId = (String)configParams.get("TauPTask");
        taskAction = commonAccess.getTaskAction(taskId);
        try {
            taupTask = (TauPTask)taskAction.getTask();
        } catch(ConfigurationException e) {
            GlobalExceptionHandler.handle("Problem loading taup task", e);
            return;
        }
        String taskID = (String)configParams.get(TASK_ID);
        TaskAction ta = commonAccess.getTaskAction(taskID);
        ta.addToToolBar(stationSelector);
        Thread t = new Thread(new ChannelChooserLoader(),
                              "Station Selector Loader");
        t.setPriority(Thread.NORM_PRIORITY - 1);
        t.start();
    }

    private class ChannelChooserLoader implements Runnable {

        public void run() {
            CommonAccess commonAccess = CommonAccess.getCommonAccess();
            String taskId = (String)configParams.get("channelChooserTask");
            TaskAction taskAction = commonAccess.getTaskAction(taskId);
            try {
                channelChooserTask = (ChannelChooserTask)taskAction.getTask();
                stationSelector.setChannelChooser(channelChooserTask.getChannelChooser());
            } catch(NoNetworkException e) {
                GlobalExceptionHandler.handle(e);
            } catch(ConfigurationException e) {
                GlobalExceptionHandler.handle("Problem loading the channel chooser",
                                              e);
            }
        }
    }

    public void invoke() throws Exception {
        if(placeholder != null) {
            placeholderPanel = placeholder.generateImagePanel();
            add(placeholderPanel);
        }
        if(!initialized) {
            if(NetworkGateKeeper.accessAllowed()) {
            } else {
                throw new NoNetworkException();
            }
            createGUI();
        }
    }

    public PlottableDisplay getDisplay() {
        return disp;
    }

    public void destroy() {
        if(reloadTimer != null) {
            reloadTimer.stop();
        }
    }

    public void createGUI() throws Exception {
        if(!initialized) {
            Date start = setTimeOnDate(ClockUtil.now(), true);
            pDateChooser = new PlottableDateChooser(start);
            ScalingSliders ss = new ScalingSliders();
            Object[] orientationNames = {DisplayUtils.UP,
                                         DisplayUtils.NORTH,
                                         DisplayUtils.EAST};
            orientationSelector = new JComboBox(orientationNames);
            JLabel orientation = new JLabel("Orientation:");
            JPanel orientationPanel = new JPanel(new BorderLayout());
            // orientationPanel.add(orientation, BorderLayout.NORTH);
            // orientationPanel.add(orientationSelector, BorderLayout.CENTER);
            GridBagLayout controlLayout = new GridBagLayout();
            JPanel controlPanel = new JPanel(controlLayout);
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.fill = GridBagConstraints.HORIZONTAL;
            gbc.weightx = 1.0;
            controlLayout.setConstraints(ss, gbc);
            controlPanel.add(ss);
            gbc.weightx = 0;
            controlLayout.setConstraints(pDateChooser, gbc);
            controlPanel.add(pDateChooser);
            controlLayout.setConstraints(orientationPanel, gbc);
            controlPanel.add(orientationPanel);
            add(controlPanel, BorderLayout.NORTH);
        }
        initialized = true;
    }

    public void loadData() {
        if(!dispAdded) {
            if(placeholderPanel != null) {
                remove(placeholderPanel);
            }
            add(new JScrollPane(disp), BorderLayout.CENTER);
            dispAdded = true;
        }
        loaderPool.invokeLater(plottableLoader);
    }

    WorkerThreadPool loaderPool = new WorkerThreadPool("Plottable Loader", 1);

    private class PlottableLoader extends AbstractJob {

        public PlottableLoader() {
            super("Plottable Loader");
            JobTracker.getTracker().add(this);
            setFinished();
        }

        public void runJob() {
            setFinished(false);
            try {
                ChannelId id = getChannelId();
                if(id == null)
                    return;
                setStatus("Getting events");
                EventAccessOperations[] events = queryEvents(pDateChooser.getStart());
                Arrival[][] arrivals = getArrivalsFromEvent(events);
                String stationName = getStationName();
                setStatus("Updating day viewer");
                disp.setPlottable(getPlottable(id),
                                  stationName,
                                  (String)orientationSelector.getSelectedItem(),
                                  pDateChooser.getStart(),
                                  id,
                                  events,
                                  arrivals);
            } catch(ConfigurationException e) {
                GlobalExceptionHandler.handle("Trouble getting events for plottable",
                                              e);
                setFinished();
                return;
            } catch(NoNetworkException e) {
                
            } catch(ChannelChooserException e) {
                GlobalExceptionHandler.handle("Trouble getting channels for plottable",
                                              e);
                setFinished();
                return;
            }
            setStatus("Starting day viewer reloader");
            if(reloadTimer != null) {
                reloadTimer.removeActionListener(dayReloader);
            } else {
                reloadTimer = new Timer(FIFTEEN_MINUTES, new ActionListener() {

                    public void actionPerformed(ActionEvent e) {}
                });
            }
            if(pDateChooser.isToday() && NetworkGateKeeper.accessAllowed()) {
                dayReloader = new DayReloader();
                reloadTimer.addActionListener(dayReloader);
                reloadTimer.start();
            }
            setFinished();
        }
    }

    private void showData(ChannelId id, String stationName) {}

    private Arrival[][] getArrivalsFromEvent(EventAccessOperations[] events) throws ChannelChooserException {
        Arrival[][] arrivals = new Arrival[events.length][];
        Channel chan = getChannel();
        for(int i = 0; i < events.length; i++) {
            arrivals[i] = taupTask.getArrivals(chan, events[i]);
        }
        return arrivals;
    }

    public Channel getChannel() throws ChannelChooserException {
        Channel[] channels = getChannels();
        String orientation = (String)orientationSelector.getSelectedItem();
        for(int i = 0; i < channels.length; i++) {
            if(DisplayUtils.getOrientationName(channels[i].get_id().channel_code)
                    .equals(orientation)) {
                return channels[i];
            }
        }
        return null;
    }

    public ChannelImpl[] getChannels() throws ChannelChooserException {
        return stationSelector.getSelectedChannels();
    }

    private ChannelId getChannelId() throws ChannelChooserException {
        Channel chan = getChannel();
        if(chan == null) {
            JOptionPane.showMessageDialog(null,
                                          "You must select a station on the map before attempting to reload the data in the day viewer",
                                          "No station selected",
                                          JOptionPane.WARNING_MESSAGE);
            return null;
        }
        return getChannel().get_id();
    }

    private String getStationName() {
        return stationSelector.getStationName();
    }

    private Plottable[] getPlottable(ChannelId id) {
        try {
            CrocusPlottableQueryParams queryParams = new CrocusPlottableQueryParams(host);
            queryParams.setPort(port);
            queryParams.setChannel_id(id).setYear(getYear()).setJday(getJulianDay())
                       .setModulo(86400/PlottableDisplay.TOTAL_WIDTH);
            
            CrocusPlottable plottableQuerier = new CrocusPlottable(queryParams);
            Plottable[] results = plottableQuerier.getPlottable();
            // no exception, so reset to zero
            consecutiveFailures = 0;
            return results;
        } catch(SeisFileException e) {
            if(consecutiveFailures++ < 5) {
                return getPlottable(id);
            }
            GlobalExceptionHandler.handle(e);
        }
        return new Plottable[0];
    }

    private int consecutiveFailures = 0;

    public void setPlottableTool(Task plottableTool) {
        if(this.plottableTool != null) {
            this.plottableTool.destroy();
        }
        this.plottableTool = plottableTool;
    }

    public Task getPlottableTool() {
        return plottableTool;
    }

    private class DayReloader implements ActionListener {

        public void actionPerformed(ActionEvent e) {
            pDateChooser.setStart(ClockUtil.now());
            new PlottableLoader().run();
        }
    }

    private int getJulianDay() {
        Date dateChosen = pDateChooser.getStart();
        Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        calendar.setTime(dateChosen);
        int jday = calendar.get(Calendar.DAY_OF_YEAR);
        return jday;
    }

    private int getYear() {
        Date dateChosen = pDateChooser.getStart();
        Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        calendar.setTime(dateChosen);
        int year = calendar.get(Calendar.YEAR);
        return year;
    }

    private EventAccessOperations[] queryEvents(Date date)
            throws ConfigurationException, NoNetworkException {
        Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        calendar.setTime(date);
        GregorianCalendar gregorianCalendarBegin = new GregorianCalendar(calendar.get(Calendar.YEAR),
                                                                         calendar.get(Calendar.MONTH),
                                                                         calendar.get(Calendar.DATE),
                                                                         0,
                                                                         0,
                                                                         0);
        GregorianCalendar gregorianCalendarEnd = new GregorianCalendar(calendar.get(Calendar.YEAR),
                                                                       calendar.get(Calendar.MONTH),
                                                                       calendar.get(Calendar.DATE),
                                                                       23,
                                                                       59,
                                                                       59);
        gregorianCalendarBegin.setTimeZone(TimeZone.getTimeZone("GMT"));
        gregorianCalendarEnd.setTimeZone(TimeZone.getTimeZone("GMT"));
        Time startTime = new MicroSecondDate(gregorianCalendarBegin.getTime()).getFissuresTime();
        Time endTime = new MicroSecondDate(gregorianCalendarEnd.getTime()).getFissuresTime();
        TimeRange timeRange = new TimeRange(startTime, endTime);
        UnitImpl depthUnit = UnitImpl.KILOMETER;
        Quantity minDepth = new QuantityImpl(0, depthUnit);
        Quantity maxDepth = new QuantityImpl(1000, depthUnit);
        String[] magTypes = new String[0];
        String[] catalogs = new String[] {"NEIC PDE"};
        String[] contributors = new String[0];
        Area area = new GlobalAreaImpl();
        EventAccessOperations[] eventAccess = queryEventsTask.queryEvents(area,
                                                                          minDepth,
                                                                          maxDepth,
                                                                          timeRange,
                                                                          magTypes,
                                                                          5.5F,
                                                                          10.0F,
                                                                          catalogs,
                                                                          contributors);
        return eventAccess;
    }

    private class ScalingSliders extends JPanel {

        private ScalingSliders() {
            int min = 25;
            int max = 525;
            int start = 100;
            JSlider ampSlider = new JSlider(JSlider.HORIZONTAL, min, max, start);
            ampSlider.setMajorTickSpacing(100);
            ampSlider.setPaintTicks(true);
            // Create the label table
            Hashtable labelTable = new Hashtable();
            labelTable.put(new Integer(min), new JLabel("Tiny"));
            labelTable.put(new Integer(max), new JLabel("Huge"));
            ampSlider.setLabelTable(labelTable);
            ampSlider.setPaintLabels(true);
            ampSlider.addChangeListener(new ChangeListener() {

                public void stateChanged(ChangeEvent ce) {
                    fireAmplitudeChanged(ce);
                }
            });
            JLabel ampLabel = new JLabel("Amplitude:");
            min = 10;
            max = 110;
            start = PlottableDisplay.OFFSET;
            JSlider spacingSlider = new JSlider(JSlider.HORIZONTAL,
                                                min,
                                                max,
                                                start);
            spacingSlider.setMajorTickSpacing(20);
            spacingSlider.setPaintTicks(true);
            // Create the label table
            labelTable = new Hashtable();
            labelTable.put(new Integer(min), new JLabel("Close"));
            labelTable.put(new Integer(max), new JLabel("Distant"));
            spacingSlider.setLabelTable(labelTable);
            spacingSlider.setPaintLabels(true);
            spacingSlider.addChangeListener(new ChangeListener() {

                public void stateChanged(ChangeEvent ce) {
                    fireSpacingChanged(ce);
                }
            });
            JLabel spacingLabel = new JLabel("Spacing:");
            GridBagLayout gb = new GridBagLayout();
            setLayout(gb);
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.fill = GridBagConstraints.HORIZONTAL;
            gbc.gridwidth = GridBagConstraints.RELATIVE;
            gb.setConstraints(ampLabel, gbc);
            add(ampLabel);
            gbc.weightx = 1.0;
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            gb.setConstraints(ampSlider, gbc);
            add(ampSlider);
            gbc.gridwidth = GridBagConstraints.RELATIVE;
            gbc.weightx = 0.0;
            gbc.ipadx = 3;
            gb.setConstraints(spacingLabel, gbc);
            add(spacingLabel);
            gbc.weightx = 1.0;
            gbc.ipadx = 0;
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            gb.setConstraints(spacingSlider, gbc);
            add(spacingSlider);
        }

        public void fireAmplitudeChanged(ChangeEvent ce) {
            float amp = ((JSlider)ce.getSource()).getValue() / 100f;
            disp.setAmpScale(amp);
        }

        public void fireSpacingChanged(ChangeEvent ce) {
            int offset = ((JSlider)ce.getSource()).getValue();
            disp.setOffset(offset);
        }
    }

    private class StationSelector extends JComboBox {

        public StationSelector() {
            addItem("Select a station in the map to load data in the day viewer");
        }

        public void setChannelChooser(ChannelChooser chanChooser) {
            this.chanChooser = chanChooser;
            chanChooser.addStationSelectionListener(new StationSelectionListener() {

                public void stationSelectionChanged(StationSelectionEvent s) {
                    MicroSecondDate date = ClockUtil.now();
                    if(pDateChooser != null) {
                        date = new MicroSecondDate(pDateChooser.getStart());
                    }
                    setStations(s.getSelectedStations(date));
                }
            });
        }

        public void setStations(Station[] stations) {
            if(stations.length > 0) {
                removeAllItems();
                Map newStationName = new HashMap();
                int j = 0;
                for(int i = 0; i < stations.length; i++) {
                    if(!newStationName.containsKey(stations[i].getName())) {
                        newStationName.put(stations[i].getName(), stations[i]);
                        addItem(stations[i].getName());
                        if(!stationName.containsKey(stations[i].getName())) {
                            setSelectedIndex(j);
                        }
                        j++;
                    }
                }
                stationName = newStationName;
                try {
                    CommonAccess.getCommonAccess()
                            .getTaskAction(PlottableTask.this)
                            .checkDisplayLocation();
                } catch(ConfigurationException e) {
                    // shouldn't be able to happen at this point. check display
                    // location must've been called by now
                    GlobalExceptionHandler.handle(e);
                }
                loadData();
            }
        }

        public ChannelImpl[] getSelectedChannels() throws ChannelChooserException {
            if(chanChooser != null) {
                StationImpl selected = stationName.get(getSelectedItem());
                if(selected != null) {
                    List<ChannelImpl> channels = chanChooser.getChannels(selected,
                                                                 new MicroSecondDate(pDateChooser.getStart()));
                    // Here's an ugly hack to get only BHZ and friends since BHZ
                    // is all delilah has...
                    List wantedChannels = new ArrayList();
                    for (ChannelImpl channelImpl : channels) {
                        if(channelImpl.get_code()
                                .toUpperCase()
                                .startsWith("BH")) {
                            wantedChannels.add(channelImpl);
                        }
                    }
                    return (ChannelImpl[])wantedChannels.toArray(new ChannelImpl[0]);
                    // return channels;
                }
            }
            return new ChannelImpl[0];
        }

        public String getStationName() {
            return (String)super.getSelectedItem();
        }

        private Map<String, StationImpl> stationName = new HashMap<String, StationImpl>();

        private ChannelChooser chanChooser;
    }

    /**
     * TODO: this pretty much copies queryEventsTask's timeEditor. the eventual
     * good thing to do would be to replace the code-stink-ridden dateChooser in
     * fissuresUtil.
     */
    private class PlottableDateChooser extends JPanel {

        public PlottableDateChooser(Date initialStartTime) {
            Box vBox = Box.createVerticalBox();
            vBox.add(Box.createVerticalGlue());
            vBox.add(new JLabel("Choose Day: "));
            DateFormat dFormat = new SimpleDateFormat("MMM dd, yyyy");
            dFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
            dateFormatter = new DateFormatter(dFormat);
            startDF = CalendarFactory.createDateField(dateFormatter);
            startDF.setValue(initialStartTime);
            vBox.add(startDF);
            vBox.add(Box.createVerticalGlue());
            Box hBox = Box.createHorizontalBox();
            hBox.add(Box.createHorizontalGlue());
            hBox.add(vBox);
            hBox.add(Box.createHorizontalGlue());
            add(hBox);
        }

        public Date getStart() {
            Date start = getDate(startDF, true);
            return start;
        }

        public Date getEnd() {
            Date end = getDate(startDF, false);
            return end;
        }

        public Date getDate(DateField control, boolean isStart) {
            try {
                return setTimeOnDate(CalendarUtils.convertToDate(control.getValue()),
                                     isStart);
            } catch(ParseException e) {
                GlobalExceptionHandler.handle("problem setting end date. returning now as the date",
                                              e);
            }
            return ClockUtil.now();
        }

        public void setStart(Date d) {
            setDate(startDF, d, true);
        }

        private void setDate(DateField control, Date d, boolean isStart) {
            control.setValue(setTimeOnDate(d, isStart));
        }

        public boolean isToday() {
            return CalendarUtils.isToday(getStart());
        }

        private DateField startDF;

        private DateFormatter dateFormatter;
    }

    public static Date setTimeOnDate(Date d, boolean isStart) {
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        cal.setTime(d);
        int hours = 0, minutes = 0, seconds = 0, millis = 0;
        if(!isStart) {
            hours = 23;
            minutes = 59;
            seconds = 59;
            millis = 999;
        }
        cal.set(Calendar.HOUR_OF_DAY, hours);
        cal.set(Calendar.MINUTE, minutes);
        cal.set(Calendar.SECOND, seconds);
        cal.set(Calendar.MILLISECOND, millis);
        MicroSecondDate adjDate = new MicroSecondDate(cal.getTime());
        return adjDate;
    }

    private static final Insets HORIZONTAL_SPACER = new Insets(0, 5, 0, 5);

    private static final Insets ZERO = new Insets(0, 0, 0, 0);

    private Map configParams;

    private PlottableDateChooser pDateChooser;

    private QueryEventsTask queryEventsTask;

    private TauPTask taupTask;

    private PlottableDisplay disp = new PlottableDisplay();

    private boolean initialized = false;

    private boolean dispAdded = false;
    
    private String host = "crocus.seis.sc.edu";
    
    private int port = 80;

    private ChannelChooserTask channelChooserTask;

    private JComboBox orientationSelector;

    static Category logger = Category.getInstance(PlottableTask.class.getName());

    private Timer reloadTimer;

    private DayReloader dayReloader;

    private StationSelector stationSelector = new StationSelector();

    private static final int FIFTEEN_MINUTES = 15 * 60 * 1000;

    private Task plottableTool = null;

    private PlaceholderImage placeholder;

    private JComponent placeholderPanel;

    private PlottableLoader plottableLoader = new PlottableLoader();
}// PlottableTask
