content: package rabbitescape.ui.swing;
  
  import java.awt.Color;
  
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.Frame;
  import java.awt.GridBagConstraints;
  import java.awt.GridBagLayout;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  
  import javax.swing.BorderFactory;
  import javax.swing.ButtonGroup;
  import javax.swing.JButton;
  import javax.swing.JDialog;
  import javax.swing.JPanel;
  import javax.swing.JRadioButton;
  import javax.swing.JScrollPane;
  import javax.swing.JSpinner;
  import javax.swing.JTextArea;
  import javax.swing.JTextField;
  import javax.swing.SpinnerModel;
  import javax.swing.SwingWorker;
  import javax.swing.Timer;
  import javax.swing.event.ChangeEvent;
  import javax.swing.event.ChangeListener;
  
  import static rabbitescape.engine.i18n.Translation.t;
  
  /**
   * @brief GUI elements for retrieving levels from issues.
   */
  public class GitHubIssueDialog extends JDialog implements ChangeListener
  {
  
      private static final long serialVersionUID = 1L;
      private JTextField issueNameBox = new JTextField( "" );
      private JTextArea issueTextBox = new JTextArea( "" );
      private JTextArea issueWorldBox = new JTextArea( "" );
      private JTextField statusBox = new JTextField(
          t( "Attempting to contact github." ) );
      private IssueSpinnerModel issueModel;
      private WorldSpinnerModel worldModel;
      private boolean choseWorld = false;
      GitHubClient ghc = null;
  
      public static enum Label
      {
          ALL,
          BUG,
          LEVEL
      };
  
      class IssueSpinnerModel implements SpinnerModel, GitHubPageFetcher
      {
          private int issueIndex = 0;
          private ChangeListener changeListener;
          private Label filterMode = Label.ALL;
  
          IssueSpinnerModel()
          {
          }
  
          /**
           * @brief
           */
          @Override
          public void notifyAndFetch( String url,
              String requestProperty,
              String notification,
              GitHubPageFetchNotifier ghpfn )
          {
              GitHubIssue ghi = issueModel.getCurrentIssue();
              if ( null == ghi )
              {
                  return;
              }
              final IssuePageFetchWorker<Void, Void> w =
                  new IssuePageFetchWorker<Void, Void>( url, requestProperty,
                      ghpfn );
              w.execute();
              statusBox.setText( notification );
              // Let dt repaint statusBox
              DotTic dt = new DotTic( w );
              dt.start();
          }
  
          @Override
          public void addChangeListener( ChangeListener cl )
          {
              changeListener = cl;
          }
  
          @Override
          public Object getNextValue()
          {
  
              return getRelativeValue( -1 );
          }
  
          @Override
          public Object getPreviousValue()
          {
              return getRelativeValue( +1 );
          }
  
          public Object getRelativeValue( int indexStep )
          {
              if ( null == ghc )
              {
                  return null;
              }
              int newIssueIndex = issueIndex + indexStep;
              GitHubIssue ghi = ghc.getIssue( newIssueIndex, this );
              if ( null == ghi )
              {
                  return null;
              }
              switch ( filterMode )
              {
              case BUG:
                  if ( !ghi.isBug() )
                  {
                      return getRelativeValue( indexStep
                          + (int)Math.signum( indexStep ) );
                  }
                  break;
              case LEVEL:
                  if ( !ghi.isLevel() )
                  {
                      return getRelativeValue( indexStep
                          + (int)Math.signum( indexStep ) );
                  }
                  break;
              case ALL:
              }
              return Integer.valueOf( ghi.getNumber() );
          }
  
          public GitHubIssue getCurrentIssue()
          {
              if ( null == ghc )
              {
                  return null;
              }
              return ghc.getIssue( issueIndex, this );
          }
  
          @Override
          public Object getValue()
          {
              GitHubIssue ghi = getCurrentIssue();
              if ( null == ghi )
              {
                  return null;
              }
              return Integer.valueOf( ghi.getNumber() );
          }
  
          @Override
          public void removeChangeListener( ChangeListener arg0 )
          {
              throw new RuntimeException(); // not used
          }
  
          public void setFilter( Label filter )
          {
              filterMode = filter;
          }
  
          @Override
          public void setValue( Object issueNumberIntegerObject )
          {
              Integer issueNumber = (Integer)issueNumberIntegerObject;
              if ( null == issueNumber )
              {
                  return;
              }
              issueIndex = ghc.getIndexOfNumber( issueNumber.intValue() );
              // new issue may have different world index current
              worldModel.setValue( worldModel.getValue() );
              changeListener.stateChanged( new ChangeEvent( this ) );
          }
  
      }
  
      class WorldSpinnerModel implements SpinnerModel
      {
          private ChangeListener changeListener;
  
          @Override
          public void addChangeListener( ChangeListener l )
          {
              changeListener = l;
          }
  
          @Override
          public Object getNextValue()
          {
              GitHubIssue ghi = issueModel.getCurrentIssue();
              if ( null == ghi )
              {
                  return null;
              }
              int value = ghi.getCurrentWorldIndex() - 1;
              return Integer.valueOf( value < 0 ? 0 : value );
          }
  
          @Override
          public Object getPreviousValue()
          {
              GitHubIssue ghi = issueModel.getCurrentIssue();
              if ( null == ghi )
              {
                  return null;
              }
              int value = ghi.getCurrentWorldIndex() + 1;
              // No bounds checking needed here, setValue's is OK.
              return Integer.valueOf( value );
          }
  
          @Override
          public Object getValue()
          {
              GitHubIssue ghi = issueModel.getCurrentIssue();
              if ( null == ghi )
              {
                  return null;
              }
              return Integer.valueOf( ghi.getCurrentWorldIndex() );
          }
  
          @Override
          public void removeChangeListener( ChangeListener l )
          {
              throw new RuntimeException(); // Not used
          }
  
          @Override
          public void setValue( Object WorldIndexIntegerObject )
          {
              Integer worldIndexInteger = (Integer)WorldIndexIntegerObject;
              if ( null == worldIndexInteger )
              {
                  return;
              }
              GitHubIssue ghi = issueModel.getCurrentIssue();
              if ( null == ghi )
              {
                  return;
              }
              if ( ghi.setCurrentWorldIndex( (int)worldIndexInteger ) )
              {
                  changeListener.stateChanged( new ChangeEvent( this ) );
              }
          }
  
      }
  
      protected GitHubIssueDialog( Frame frame )
      {
          super( frame, true ); // arg2 sets modal
          setTitle( t(
              "Retrieve level from the GitHub Rabbit Escape issue pages." ) );
  
          GridBagConstraints gbc = new GridBagConstraints();
  
          JPanel pane = new JPanel( new GridBagLayout() );
  
          issueModel = new IssueSpinnerModel();
          JSpinner issueSpinner = new JSpinner( issueModel );
          issueSpinner.setPreferredSize( new Dimension( 60, 30 ) );
          issueSpinner.addChangeListener( this );
  
          worldModel = new WorldSpinnerModel();
          JSpinner worldSpinner = new JSpinner( worldModel );
          worldSpinner.setPreferredSize( new Dimension( 60, 30 ) );
          worldSpinner.addChangeListener( this );
  
          JRadioButton levelFilterButton = new JRadioButton( t( "Level" ) );
          JRadioButton bugFilterButton = new JRadioButton( t( "Bug" ) );
          JRadioButton allFilterButton = new JRadioButton( t( "All" ) );
          ButtonGroup filterButtons = new ButtonGroup();
          filterButtons.add( levelFilterButton );
          filterButtons.add( bugFilterButton );
          filterButtons.add( allFilterButton );
          allFilterButton.doClick(); // set preselected filter
          JButton fetchFollowButton =
              new JButton( t( "Fetch followup comments" ) );
          JPanel spacerPanel = new JPanel();
          // Ater packing, this gives the dialog it's width
          spacerPanel.setPreferredSize( new Dimension( 500, 2 ) );
          JButton okButton = new JButton( t( "OK" ) );
          JButton cancelButton = new JButton( t( "Cancel" ) );
  
          statusBox.setEditable( false );
          issueNameBox.setEditable( false );
          issueTextBox.setEditable( false );
          JScrollPane issueTextScrollPane = new JScrollPane( issueTextBox );
          issueTextScrollPane.setPreferredSize( new Dimension( 300, 300 ) );
          issueTextBox.setBorder( BorderFactory.createLineBorder( Color.GRAY ) );
          issueWorldBox.setEditable( false );
          issueWorldBox.setBorder( BorderFactory.createLineBorder( Color.GRAY ) );
          issueWorldBox.setFont( new Font( "monospaced", Font.PLAIN, 12 ) );
          // After packing, this gives the dialog it's height
          issueWorldBox.setPreferredSize( new Dimension( 2, 500 ) );
  
          gbc.fill = GridBagConstraints.BOTH;
  
          gbc.gridx = 0;
          gbc.gridy = 0;
          gbc.gridheight = 2;
          pane.add( issueSpinner, gbc );
          gbc.gridheight = 1;
  
          gbc.gridx = 0;
          gbc.gridy = 2;
          pane.add( worldSpinner, gbc );
  
          gbc.gridx = 1;
          gbc.gridy = 0;
          pane.add( levelFilterButton, gbc );
  
          gbc.gridx = 1;
          gbc.gridy = 1;
          pane.add( bugFilterButton, gbc );
  
          gbc.gridx = 1;
          gbc.gridy = 2;
          pane.add( allFilterButton, gbc );
  
          gbc.gridx = 2;
          gbc.gridy = 0;
          pane.add( fetchFollowButton, gbc );
  
          gbc.gridwidth = 3;
          gbc.gridx = 3;
          gbc.gridy = 0;
          pane.add( statusBox, gbc );
          gbc.gridwidth = 1;
  
          gbc.gridwidth = 3;
          gbc.gridx = 3;
          gbc.gridy = 1;
          pane.add( issueNameBox, gbc );
          gbc.gridwidth = 1;
  
          gbc.gridx = 3;
          gbc.gridy = 2;
          pane.add( spacerPanel, gbc );
  
          gbc.fill = GridBagConstraints.NONE;
  
          gbc.gridx = 4;
          gbc.gridy = 2;
          pane.add( okButton, gbc );
  
          gbc.gridx = 5;
          gbc.gridy = 2;
          pane.add( cancelButton, gbc );
  
          gbc.fill = GridBagConstraints.BOTH;
  
          gbc.gridwidth = 3;
          gbc.gridx = 0;
          gbc.gridy = 3;
          pane.add( issueTextScrollPane, gbc );
          gbc.gridwidth = 1;
  
          gbc.gridwidth = 3;
          gbc.gridheight = 1;
          gbc.gridx = 3;
          gbc.gridy = 3;
          pane.add( issueWorldBox, gbc );
          gbc.gridwidth = 1;
          gbc.gridheight = 1;
  
          setLayout( new GridBagLayout() );
          gbc.gridx = 0;
          gbc.gridy = 0;
          gbc.fill = GridBagConstraints.BOTH;
          this.add( pane, gbc );
  
          levelFilterButton.addActionListener( new ActionListener()
          {
              @Override
              public void actionPerformed( ActionEvent e )
              {
                  issueModel.setFilter( Label.LEVEL );
              }
          } );
          bugFilterButton.addActionListener( new ActionListener()
          {
              @Override
              public void actionPerformed( ActionEvent e )
              {
                  issueModel.setFilter( Label.BUG );
              }
          } );
          allFilterButton.addActionListener( new ActionListener()
          {
              @Override
              public void actionPerformed( ActionEvent e )
              {
                  issueModel.setFilter( Label.ALL );
              }
          } );
          fetchFollowButton.addActionListener( new ActionListener()
          {
              @Override
              public void actionPerformed( ActionEvent e )
              {
                  fetchFollowupComments();
              }
          } );
          okButton.addActionListener( new ActionListener()
          {
              @Override
              public void actionPerformed( ActionEvent e )
              {
                  choseWorld = true;
                  setVisible( false );
              }
          } );
          cancelButton.addActionListener( new ActionListener()
          {
              @Override
              public void actionPerformed( ActionEvent e )
              {
                  setVisible( false );
              }
          } );
  
          setResizable( false ); // / @TODO maybe make it resize so the
                                 // worldTextBox expands
          pack();
          setLocationRelativeTo( frame );
          stateChanged( null ); // show initial values for GUI items
          ghc = new GitHubClient();
          final InitFetchWorker<Void, Void> ifw =
              new InitFetchWorker<Void, Void>();
          ifw.execute();
          DotTic dt = new DotTic( (SwingWorker<Void, Void>)ifw );
          dt.start();
          setVisible( true );
  
      }
  
      public String getWorld()
      {
          GitHubIssue ghi = issueModel.getCurrentIssue();
          if ( !choseWorld || ( null == ghi ) ) // user cancelled or maybe no inet
          {
              return null;
          }
          String wrappedWorld = ghi.getCurrentWorld();
          if ( null == wrappedWorld )
          {
              return null;
          }
          return wrappedWorld;
      }
  
      public String generateFilename()
      {
          String title = issueModel.getCurrentIssue().getTitle();
          if ( null == title )
          {
              title = t( "Issue has no title" );
          }
          title = title.replaceAll( "\\W", "" );
          return title;
      }
  
      /**
       * @brief github only sends the opening comment in te first instance.
       */
      public void fetchFollowupComments()
      {
          GitHubIssue ghi = issueModel.getCurrentIssue();
          if ( null == ghi )
          {
              return;
          }
          final FetchCommentsWorker<Void, Void> fcw =
              new FetchCommentsWorker<Void, Void>( ghi );
          fcw.execute();
          statusBox.setText( t( "Fetching followup comments for" ) + " #"
              + ghi.getNumber()
              + "." );
          // Let dt repaint statusBox
          DotTic dt = new DotTic( fcw );
          dt.start();
      }
  
      /**
       * @brief Listens to the issue choosing spinner and world choosing spinner
       */
      @Override
      public void stateChanged( ChangeEvent e )
      {
          GitHubIssue ghi = issueModel.getCurrentIssue();
          if ( null == ghi )
          {
              return;
          }
  
          issueNameBox.setText( ghi.getTitle() );
          issueNameBox.repaint();
  
          String worldText = ghi.getCurrentWorld(); // /
          if ( null == worldText ) // some issues have no worlds
          {
              issueWorldBox.setText( "" );
          }
          {
              issueWorldBox.setText( worldText );
          }
          issueWorldBox.repaint();
  
          issueTextBox.setText( ghi.getBody() );
          issueTextBox.repaint();
      }
  
      private final class DotTic implements ActionListener
      {
          private Timer timer;
          private SwingWorker<Void, Void> worker;
  
          public DotTic( SwingWorker<Void, Void> ifw )
          {
              timer = new Timer( 800, this );
              worker = ifw;
          }
  
          public void start()
          {
              timer.start();
          }
  
          @Override
          public void actionPerformed( ActionEvent event )
          {
              statusBox.setText( statusBox.getText() + "." );
              statusBox.repaint();
              if ( worker.isDone() )
              {
                  timer.stop();
                  if ( !"".equals( ghc.getError() ) )
                  {
                      statusBox.setText( ghc.getError() );
                  }
                  else
                  {
                      statusBox.setText( statusBox.getText() + t( "Done." ) );
                  }
                  statusBox.repaint();
                  stateChanged( null ); // update info in dialog with new info
                                        // from github
              }
          }
      }
  
      private final class InitFetchWorker<T, U> extends SwingWorker<Void, Void>
      {
          @Override
          protected Void doInBackground() throws Exception
          {
              ghc.initialise();
              return null;
          }
      }
  
      private final class IssuePageFetchWorker<T, U>
          extends
          SwingWorker<Void, Void>
      {
          String u;
          String rp;
          GitHubPageFetchNotifier ghpfn;
  
          public IssuePageFetchWorker( String url, String requestProperty,
              GitHubPageFetchNotifier notifier )
          {
              u = url;
              rp = requestProperty;
              ghpfn = notifier;
          }
  
          @Override
          protected Void doInBackground() throws Exception
          {
              String answer = HttpTools.get( u, rp );
              ghpfn.setPage( answer );
              return null;
          }
      }
  
      private final class FetchCommentsWorker<T, U>
          extends
          SwingWorker<Void, Void>
      {
          private GitHubIssue ghi;
  
          public FetchCommentsWorker( GitHubIssue i )
          {
              ghi = i;
          }
  
          @Override
          protected Void doInBackground() throws Exception
          {
              ghi.fetchComments( ghc );
              return null;
          }
      }
  }
  