/*
Copyright 2012 Esri

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.‚Äč
*/
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
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.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;

import com.esri.client.toolkit.overlays.InfoPopupOverlay;
import com.esri.core.geometry.Envelope;
import com.esri.core.geometry.Geometry;
import com.esri.core.geometry.GeometryEngine;
import com.esri.core.geometry.Point;
import com.esri.core.geometry.SpatialReference;
import com.esri.core.map.CallbackListener;
import com.esri.core.map.FeatureSet;
import com.esri.core.map.Graphic;
import com.esri.core.renderer.UniqueValueInfo;
import com.esri.core.renderer.UniqueValueRenderer;
import com.esri.core.symbol.SimpleFillSymbol;
import com.esri.core.symbol.SimpleLineSymbol;
import com.esri.core.symbol.SimpleMarkerSymbol;
import com.esri.core.symbol.SimpleMarkerSymbol.Style;
import com.esri.core.symbol.Symbol;
import com.esri.core.tasks.ags.geoprocessing.GPFeatureRecordSetLayer;
import com.esri.core.tasks.ags.geoprocessing.GPParameter;
import com.esri.core.tasks.ags.geoprocessing.GPString;
import com.esri.core.tasks.ags.geoprocessing.Geoprocessor;
import com.esri.core.tasks.ags.query.OrderByFields;
import com.esri.core.tasks.ags.query.Query;
import com.esri.core.tasks.ags.query.QueryTask;
import com.esri.map.ArcGISFeatureLayer;
import com.esri.map.ArcGISTiledMapServiceLayer;
import com.esri.map.GraphicsLayer;
import com.esri.map.JMap;
import com.esri.map.MapEvent;
import com.esri.map.MapEventListenerAdapter;

/**
 * Application for Demo Theatre at Esri Dev Summit 2013. 
 * 
 * The first version checked-in is a copy of the DemoTheatreApp. Check-ins will be made
 * to show a step-by-step improvement of this application.
 */
public class DemoTheatreAppImproved {

  // JMap
  private JMap map;

  // progress bar
  private JProgressBar progressBar;

  private GraphicsLayer graphicsLayer;

  private ArcGISFeatureLayer featureLayer;

  private AtomicInteger tasksInProgress = new AtomicInteger(0);

  private JTextField queryText = new JTextField("100000");

  private DefaultTableModel tblModelCities;

  private MouseActionHandler mouseActionHandler;

  private InfoPopupOverlay infoPopupOverlay;

  // ------------------------------------------------------------------------
  // Static methods
  // ------------------------------------------------------------------------
  /**
   * Starting point of this application.
   * 
   * @param args arguments to this application.
   */
  public static void main(String[] args) {

    // Tip: Use HTTP request intercepter such as Fiddler to track requests
    // ProxySetup.setupProxy("localhost", 8888);

    // create the UI, including the map, for the application
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        try {
          DemoTheatreAppImproved app = new DemoTheatreAppImproved();
          JFrame appWindow = app.createWindow();
          appWindow.add(app.createUI());
          appWindow.setVisible(true);
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    });
  }

  // ------------------------------------------------------------------------
  // Public methods
  // ------------------------------------------------------------------------
  /**
   * Creates and displays the UI, including the map, for this application.
   * 
   * @return the UI for this sample.
   */
  public JComponent createUI() {
    // application content
    JComponent contentPane = createContentPane();

    // progress bar
    progressBar = createProgressBar(contentPane);

    // map
    map = createMap();

    // scrollable-table to display the query result
    tblModelCities = new DefaultTableModel() {
      private static final long serialVersionUID = 1L;

      @Override
      public boolean isCellEditable(int rowIndex, int mColIndex) {
        return false;
      }
    };
    // add the column headers
    tblModelCities.addColumn("City");
    tblModelCities.addColumn("Population");
    tblModelCities.addColumn("Shape");
    tblModelCities.addColumn("Capital?");
    tblModelCities.addColumn("State");
    
    final JTable tblCities = new JTable(tblModelCities);
    JScrollPane tblStateInfoScrollable = new JScrollPane(tblCities);
    tblCities.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
      @Override
      public void valueChanged(ListSelectionEvent e) {
        int selectedCityRowId = tblCities.getSelectedRow();
        if (selectedCityRowId > 0) {
          Point selectedCity = (Point) tblModelCities.getValueAt(selectedCityRowId, 2);
          highlightGeometry(selectedCity);
        }
      }
    });
    // don't show shape column
    TableColumn shapeColumn = tblCities.getColumnModel().getColumn(2);
    tblCities.getColumnModel().removeColumn(shapeColumn);

    // various actions
    JLabel queryLabel = new JLabel("Filter cities with population > ");
    queryLabel.setForeground(Color.WHITE);
    queryText.setMinimumSize(new Dimension(150, 25));
    queryText.setMaximumSize(new Dimension(150, 25));
    queryText.setColumns(10);
    JButton btnFilter = new JButton("Filter");
    btnFilter.setFocusPainted(false);
    btnFilter.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        onQuery();
      }
    });

    JButton btnAddGraphic = new JButton("Add Graphic");
    btnAddGraphic.setFocusPainted(false);
    btnAddGraphic.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        mouseActionHandler.setAction(Action.ADD_GRAPHIC);
        if (infoPopupOverlay != null) {
          infoPopupOverlay.setActive(false);
        }
      }
    });

    JButton btnDriveTime = new JButton("Drive Time");
    btnDriveTime.setFocusPainted(false);
    btnDriveTime.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        mouseActionHandler.setAction(Action.ANALYZE_DRIVE_TIME);
        if (infoPopupOverlay != null) {
          infoPopupOverlay.setActive(false);
        }
      }
    });

    JButton btnViewFeature = new JButton("View Feature");
    btnViewFeature.setFocusPainted(false);
    btnViewFeature.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        mouseActionHandler.setAction(Action.IGNORE);
        if (infoPopupOverlay != null) {
          infoPopupOverlay.setActive(true);
        }
      }
    });

    // top panel for actions
    JPanel topPanel = new JPanel();
    topPanel.setBackground(Color.BLACK);
    topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.X_AXIS));
    topPanel.add(Box.createHorizontalGlue());
    topPanel.add(queryLabel);
    topPanel.add(queryText);
    topPanel.add(btnFilter);
    topPanel.add(btnAddGraphic);
    topPanel.add(btnDriveTime);
    topPanel.add(btnViewFeature);
    topPanel.add(Box.createHorizontalGlue());

    // panel to contain query results
    JPanel bottomPanel = new JPanel();
    bottomPanel.setLayout(new BorderLayout());
    bottomPanel.add(progressBar, BorderLayout.NORTH);
    bottomPanel.add(tblStateInfoScrollable, BorderLayout.CENTER);
    bottomPanel.setPreferredSize(new Dimension(600, 150));

    contentPane.add(topPanel, BorderLayout.NORTH);
    contentPane.add(map, BorderLayout.CENTER);
    contentPane.add(bottomPanel, BorderLayout.SOUTH);

    return contentPane;
  }

  // ------------------------------------------------------------------------
  // Private methods
  // ------------------------------------------------------------------------
  private void highlightGeometry(Point point) {
    if (point == null) {
      return;
    }

    graphicsLayer.removeAll();
    graphicsLayer.addGraphic(
      new Graphic(point, new SimpleMarkerSymbol(Color.CYAN, 20, SimpleMarkerSymbol.Style.CIRCLE)));
    
    // -----------------------------------------------------------------------------------------
    // Zoom to the highlighted graphic
    // -----------------------------------------------------------------------------------------
    // from base map's information, zoom to a resolution so that city is in focus
    map.zoomToResolution(0.02197265625, point);
  }

  /**
   * Creates a map.
   * 
   * @return a map.
   */
  private JMap createMap() {

    final JMap jMap = new JMap();

    jMap.addMapEventListener(new MapEventListenerAdapter() {
      @Override
      public void mapReady(final MapEvent arg0) {
        SwingUtilities.invokeLater(new Runnable() {
          @Override
          public void run() {
            try {
              Envelope initialExtent = new Envelope(-122.593, 37.642, -122.256, 37.867); 
              // Tip: Use appropriate spatial reference 
              SpatialReference inSR = SpatialReference.create(4326); 
              initialExtent = (Envelope) GeometryEngine.project(initialExtent, inSR, jMap.getSpatialReference()); 
              ((JMap) arg0.getSource()).setExtent(initialExtent);
            } catch (Exception ex) {
              ex.printStackTrace();
            }
          }
        });
      }

      @Override
      public void mapExtentChanged(final MapEvent arg0) {
        // System.out.println("Extent: " + jMap.getScale());
      }
    });

    // -----------------------------------------------------------------------------------------
    // remote map service layer - base layer with US topology
    // -----------------------------------------------------------------------------------------
    addBaseLayer(jMap);

    // -----------------------------------------------------------------------------------------
    // graphics layer - to add result from the geoprocessing execution
    // -----------------------------------------------------------------------------------------
    /*graphicsLayer = new GraphicsLayer();
    jMap.getLayers().add(graphicsLayer);*/

    featureLayer = new ArcGISFeatureLayer(
       "http://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer/0");
    // "http://sampleserver6.arcgisonline.com/arcgis/rest/services/WorldTimeZones/MapServer/0");

    // Tip: consider using renderer
    final Symbol SYM_CAPITAL = new SimpleMarkerSymbol(Color.RED, 14, Style.TRIANGLE);
    final SimpleMarkerSymbol SYM_NON_CAPITAL = new SimpleMarkerSymbol(Color.YELLOW, 9, Style.CIRCLE); 
    UniqueValueRenderer uvRenderer = new UniqueValueRenderer(); 
    uvRenderer.setAttributeName1("capital"); 
    uvRenderer.addValue(new UniqueValueInfo(new Object[] {"Y"}, SYM_CAPITAL)); 
    uvRenderer.addValue(new UniqueValueInfo(new Object[] {"N"}, SYM_NON_CAPITAL)); 
    featureLayer.setRenderer(uvRenderer);
    
    // Tip: consider on-demand mode
    // featureLayer.setOperationMode(QueryMode.ON_DEMAND);

    jMap.getLayers().add(featureLayer);

    // Tip: order of layers -> order of graphics
    graphicsLayer = new GraphicsLayer();
    jMap.getLayers().add(graphicsLayer);

    // -----------------------------------------------------------------------------------------
    // geoprocessing service executor
    // -----------------------------------------------------------------------------------------
    mouseActionHandler = new MouseActionHandler(jMap, graphicsLayer);
    mouseActionHandler.setAction(Action.IGNORE);
    jMap.addMouseListener(mouseActionHandler);

    // Tip: get code for infopopup from samples
    addInfopopupOverlay(jMap, featureLayer);

    return jMap;
  }
  
  private void addInfopopupOverlay(JMap jMap, ArcGISFeatureLayer featureLayer) {
    // create the infopopup overlay
    infoPopupOverlay = new InfoPopupOverlay();
    // customize the popup and item titles
    infoPopupOverlay.setPopupTitle("Feature");
    // infoPopupOverlay.setItemTitle("Block: {BLOCK}");
    // add the layer of interest to the overlay
    infoPopupOverlay.addLayer(featureLayer);
    // add the overlay to the map
    jMap.addMapOverlay(infoPopupOverlay);
  }

  /**
   * Creates a window.
   * 
   * @return a window.
   */
  private JFrame createWindow() {
    JFrame window = new JFrame("Drive Time Geoprocessing Application");
    window.setBounds(100, 100, 1000, 700);
    window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    window.getContentPane().setLayout(new BorderLayout(0, 0));
    window.addWindowListener(new WindowAdapter() {
      @Override
      public void windowClosing(WindowEvent windowEvent) {
        super.windowClosing(windowEvent);
        map.dispose();
      }
    });
    return window;
  }

  /**
   * Creates a content pane.
   * 
   * @return a content pane.
   */
  private static JLayeredPane createContentPane() {
    JLayeredPane contentPane = new JLayeredPane();
    contentPane.setBounds(100, 100, 1000, 700);
    contentPane.setLayout(new BorderLayout(0, 0));
    contentPane.setVisible(true);
    return contentPane;
  }


  private void addBaseLayer(JMap jMap) {
    /*final ArcGISDynamicMapServiceLayer baseLayer = new ArcGISDynamicMapServiceLayer(
        "http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Specialty/ESRI_StateCityHighway_USA/MapServer");
*/
    // Tip: Use tiled layer as a basemap    
    final ArcGISTiledMapServiceLayer baseLayer = new ArcGISTiledMapServiceLayer(
      //"http://services.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer");
      "http://services.arcgisonline.com/ArcGIS/rest/services/ESRI_StreetMap_World_2D/MapServer"); // SR 4326
    
    jMap.getLayers().add(baseLayer);     
  }

  private void onQuery() {
    tblModelCities.setRowCount(0);

    // return if input text is not valid
    if (queryText.getText() == null || queryText.getText().isEmpty()) {
      return;
    }

    // query paremters
    Map<String, OrderByFields> sortOrder = new HashMap<String, OrderByFields>();
    sortOrder.put("pop2000", OrderByFields.DESC);
    Query query = new Query();
    query.setWhere("pop2000 > " + queryText.getText());
    query.setOrderByFields(sortOrder);
    query.setOutFields(new String[] {"*"});

    // Tip: use map's SR
    //query.setOutSpatialReference(SpatialReference.create(3857));
    query.setOutSpatialReference(map.getSpatialReference());

    // Tip: don't fetch geometry if not required
    // query.setReturnGeometry(false);

    FeatureSet queryResult;

    // execute the query
    QueryTask task = new QueryTask(featureLayer.getUrl());
    try {
      queryResult = task.execute(query);

      if (queryResult == null) {
        return;
      }

      for (Graphic result : queryResult.getGraphics()) {
        tblModelCities.addRow(new Object[] { 
          result.getAttributeValue("areaname"),
          result.getAttributeValue("pop2000"), 
          result.getGeometry(),
          result.getAttributeValue("capital"),
          result.getAttributeValue("st")});
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  

  /**
   * Creates a progress bar.
   * 
   * @param parent progress bar's parent. The horizontal axis of the progress bar will be center-aligned to the parent.
   * @return a progress bar.
   */
  private static JProgressBar createProgressBar(final JComponent parent) {
    final JProgressBar progressBar = new JProgressBar();
    progressBar.setSize(260, 20);
    parent.addComponentListener(new ComponentAdapter() {
      @Override
      public void componentResized(ComponentEvent e) {
        progressBar.setLocation(
          parent.getWidth() / 2 - progressBar.getWidth() / 2,
          parent.getHeight() - progressBar.getHeight() - 20);
      }
    });
    progressBar.setStringPainted(true);
    progressBar.setIndeterminate(true);
    progressBar.setVisible(false);
    return progressBar;
  }

  /**
   * Updates progress bar UI from the Swing's Event Dispatch Thread.
   * 
   * @param str string to be set.
   * @param visible flag to indicate visibility of the progress bar.
   */
  private void updateProgresBarUI(final String str, final boolean visible) {
    // Tip: Update UI in the UI thread
    SwingUtilities.invokeLater(new Runnable() {
      @Override 
      public void run() { 
        if (str != null) { 
          progressBar.setString(str); 
        } 
        progressBar.setVisible(visible); 
      }
    });
  }

  public static enum Action {
    ADD_GRAPHIC, ANALYZE_DRIVE_TIME, IGNORE
  }

  /**
   * Class to handle mouse events based on current Action.
   * 
   * - if action is ADD_GRAPHIC, then adds a graphic. 
   * - if action is ANALYZE_DRIVE_TIME, then runs a geoprocessing task for drive time calculations.
   */
  class MouseActionHandler extends MouseInputAdapter {

    private Action currentAction = Action.ANALYZE_DRIVE_TIME;
    
    // URL to remote geoprocessing service
    private final String URL_GEOPROCESSING_SERVICE = 
      "http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Network/ESRI_DriveTime_US/GPServer/CreateDriveTimePolygons";

    // symbology
    private final SimpleMarkerSymbol SYM_START_POINT = new SimpleMarkerSymbol(Color.RED, 16, Style.DIAMOND);

    private final SimpleLineSymbol SYM_ZONE_BORDER = new SimpleLineSymbol(Color.RED, 1);

    private final SimpleFillSymbol[] zoneFillSymbols = new SimpleFillSymbol[] { 
      new SimpleFillSymbol(new Color(255, 0, 0, 80), SYM_ZONE_BORDER), 
      new SimpleFillSymbol(new Color(255, 165, 0, 80), SYM_ZONE_BORDER), 
      new SimpleFillSymbol(new Color(0, 255, 0, 80), SYM_ZONE_BORDER) };

    JMap jMap;

    GraphicsLayer graphicsLayer;

    /**
     * Creates an object that executes the remote geoprocessing service to calculate zones with different drive times.
     * 
     * @param jMap map to get the start point of drive, and to get the spatial reference.
     * @param graphicsLayer graphics layer to which the result will be added.
     */
    public MouseActionHandler(JMap jMap, GraphicsLayer graphicsLayer) {
      this.jMap = jMap;
      this.graphicsLayer = graphicsLayer;
    }

    public void setAction(Action action) {
      currentAction = action;
    }

    public Action getAction() {
      return currentAction;
    }

    /**
     * Computes the drive time zones on click of the mouse.
     */
    @Override
    public void mouseClicked(MouseEvent mapEvent) {

      super.mouseClicked(mapEvent);
     
      if (mapEvent.getButton() == MouseEvent.BUTTON3) {
        // remove zones from previous computation
        graphicsLayer.removeAll();
        return;
      }
      
      if (currentAction == Action.IGNORE) {
        return;
      } else if (currentAction == Action.ADD_GRAPHIC) {
        handleAddGraphic(mapEvent);        
      } else {
        handleAnalyzeDriveTime(mapEvent);  
      }
    }
    
    private void handleAddGraphic(MouseEvent mapEvent) {
      Point p = jMap.toMapPoint(mapEvent.getPoint().x, mapEvent.getPoint().y);
      // Tip: use the right symbol for geometry
      SimpleMarkerSymbol s = new SimpleMarkerSymbol(Color.RED, 14, Style.CROSS);
      //SimpleLineSymbol s = new SimpleLineSymbol(Color.RED, 10);
      graphicsLayer.addGraphic(new Graphic(p, s));  
    }
    
    private void handleAnalyzeDriveTime(MouseEvent mapEvent) {
      tasksInProgress.incrementAndGet();
      updateProgresBarUI("Computing drive time zones...", true);

      // the click point is the starting point
      Point startPoint = jMap.toMapPoint(mapEvent.getX(), mapEvent.getY());
      Graphic startPointGraphic = new Graphic(startPoint, SYM_START_POINT);
      graphicsLayer.addGraphic(startPointGraphic);

      executeDriveTimes(startPointGraphic);
    }

    private void executeDriveTimes(Graphic startPointGraphic) {

      // create a Geoprocessor that points to the remote geoprocessing service.
      Geoprocessor geoprocessor = new Geoprocessor(URL_GEOPROCESSING_SERVICE);
      // set the output and process spatial reference to the map's spatial reference
      SpatialReference outSR = SpatialReference.create(4326);
      Geometry projectedStartPoint = GeometryEngine.project(
        startPointGraphic.getGeometry(), jMap.getSpatialReference(), outSR);
      Graphic projectedStartPointGraphic = new Graphic(projectedStartPoint, startPointGraphic.getSymbol());
      geoprocessor.setOutSR(outSR);
      geoprocessor.setProcessSR(outSR);

      // initialize the required input parameters: refer to help link in the
      // geoprocessing service URL for a list of required parameters
      List<GPParameter> gpInputParams = new ArrayList<GPParameter>();

      GPFeatureRecordSetLayer gpInputStartpoint = new GPFeatureRecordSetLayer("Input_Location");
      gpInputStartpoint.addGraphic(projectedStartPointGraphic);

      //GPString gpInputDriveTimes = new GPString("Drive_Time");
      // Tip: use GP service info to get the parameter names
      GPString gpInputDriveTimes = new GPString("Drive_Times");
      
      gpInputDriveTimes.setValue("1 2 3");

      gpInputParams.add(gpInputStartpoint);
      gpInputParams.add(gpInputDriveTimes);

      // execute the geoprocessing request
      /*try {
        GPParameter[] result = geoprocessor.execute(gpInputParams);
        updateProgresBarUI(null,  tasksInProgress.decrementAndGet() > 0);
        processResult(result);
      } catch (Exception ex) {
        JOptionPane.showMessageDialog(map, ex.getMessage(), "", JOptionPane.ERROR_MESSAGE);
      }*/
      
      // Tip: Do not block UI thread. 
      geoprocessor.executeAsync( 
        gpInputParams, 
        new CallbackListener<GPParameter[]>() {
            @Override 
            public void onError(Throwable th) { 
              th.printStackTrace(); 
            }
            @Override 
            public void onCallback(GPParameter[] result) { 
              updateProgresBarUI(null, tasksInProgress.decrementAndGet() > 0); 
              processResult(result); 
              } 
            } 
        );       
    }

    /**
     * Process result from geoprocessing execution.
     * 
     * @param result output of geoprocessing execution.
     */
    private void processResult(GPParameter[] result) {
      for (GPParameter outputParameter : result) {
        if (outputParameter instanceof GPFeatureRecordSetLayer) {
          GPFeatureRecordSetLayer gpLayer = (GPFeatureRecordSetLayer) outputParameter;
          int zone = 0;
          // get all the graphics and add them to the graphics layer.
          // there will be one graphic per zone.
          for (Graphic graphic : gpLayer.getGraphics()) {
            SpatialReference fromSR = SpatialReference.create(4326);
            Geometry g = graphic.getGeometry();
            Geometry pg = GeometryEngine.project(g, fromSR, jMap.getSpatialReference());
            Graphic theGraphic = new Graphic(pg, zoneFillSymbols[zone++]);
            // add to the graphics layer
            graphicsLayer.addGraphic(theGraphic);
          }
        }
      }
    }
  }
}