content: package rabbitescape.ui.swing;
  
  import java.io.PrintStream;
  import java.lang.reflect.InvocationTargetException;
  import java.util.Map;
  
  import javax.swing.JOptionPane;
  import javax.swing.SwingUtilities;
  
  import static rabbitescape.engine.i18n.Translation.t;
  import static rabbitescape.engine.util.Util.map;
  
  import rabbitescape.engine.LevelWinListener;
  import rabbitescape.engine.Token;
  import rabbitescape.engine.World;
  import rabbitescape.engine.config.Config;
  import rabbitescape.engine.menu.LevelMenuItem;
  import rabbitescape.engine.menu.LevelsMenu;
  import rabbitescape.engine.solution.PlaceTokenAction;
  import rabbitescape.engine.solution.SolutionDemo;
  import rabbitescape.engine.solution.SolutionInterpreter;
  import rabbitescape.engine.solution.SolutionRecorder;
  import rabbitescape.engine.solution.SolutionRecorderTemplate;
  
  import rabbitescape.engine.util.Util;
  import rabbitescape.render.GameLaunch;
  import rabbitescape.render.androidlike.Sound;
  import rabbitescape.render.gameloop.GameLoop;
  import rabbitescape.render.gameloop.GeneralPhysics;
  import rabbitescape.render.gameloop.Physics;
  import rabbitescape.render.gameloop.WaterAnimation;
  import rabbitescape.ui.swing.SwingGameInit.WhenUiReady;
  
  public class SwingGameLaunch implements GameLaunch
  {
      /**
       * A loop that just draws the game window when it's behind the
       * intro dialog.
       */
      class MiniGameLoop implements Runnable
      {
          public boolean running = true;
  
          @Override
          public void run()
          {
              while ( running )
              {
                  graphics.draw( physics.frameNumber() );
                  try
                  {
                      Thread.sleep( 500 );
                  }
                  catch ( InterruptedException e )
                  {
                      e.printStackTrace();
                  }
              }
          }
      }
  
      public static final String NOT_DEMO_MODE = "NOT_DEMO_MODE";
      public final World world;
      private final GeneralPhysics physics;
      public final SwingGraphics graphics;
      private final GameUi jframe;
  
      private final GameLoop loop;
      private final MainJFrame frame;
      public final SolutionRecorderTemplate solutionRecorder;
      private final SwingPlayback swingPlayback;
      private final FrameDumper frameDumper;
      private final LevelMenuItem menuItem;
      private final LevelsMenu menu;
  
      public SwingGameLaunch(
          SwingGameInit init,
          World world,
          LevelWinListener winListener,
          Sound sound,
          Config config,
          PrintStream debugout,
          String solution,
          boolean frameDumping,
          LevelMenuItem menuItem,
          LevelsMenu menu
      )
      {
          this.world = world;
  
          SolutionInterpreter solutionInterpreter =
              createSolutionInterpreter( solution, world );
  
          this.frame = init.frame;
          this.solutionRecorder = new SolutionRecorder();
          this.menuItem = menuItem;
          this.menu = menu;
          if ( frameDumping )
          {
              this.frameDumper = new FrameDumper();
          }
          else
          {
              this.frameDumper = FrameDumper.createInactiveDumper();
          }
          this.swingPlayback = new SwingPlayback( this );
          WaterAnimation waterAnimation = new WaterAnimation( world, config ) ;
          this.physics = new GeneralPhysics(
              world,
              waterAnimation,
              winListener,
              false,
              solutionRecorder,
              solutionInterpreter,
              swingPlayback,
              frameDumping
          );
          // This blocks until the UI is ready:
          WhenUiReady uiPieces = init.waitForUi.waitForUi();
  
          this.jframe = uiPieces.jframe;
          this.graphics = new SwingGraphics(
              world,
              uiPieces.jframe,
              uiPieces.bitmapCache,
              sound,
              frameDumper,
              waterAnimation
          );
  
          // Used for redraw after window is resized.
          frame.setGraphics( graphics );
  
          jframe.setGameLaunch( this );
  
          sound.setMusic( world.music );
  
          loop = new GameLoop( 
              new SwingInput(), 
              physics, 
              waterAnimation,
              graphics, 
              config, 
              debugout 
          );
      }
  
      public GameUi getUi()
      {
          return jframe;
      }
  
      public void toggleSpeed()
      {
          physics.fast = !physics.fast;
      }
  
      private static SolutionInterpreter createSolutionInterpreter(
          String solution, 
          World world 
      )
      {
          if ( solution.equals( NOT_DEMO_MODE ) )
          {
              return SolutionInterpreter.getNothingPlaying();
          }
          SolutionDemo demo = new SolutionDemo( solution, world );
          return new SolutionInterpreter( demo.solution );
      }
  
      public void stop()
      {
          loop.pleaseStop();
      }
  
      @Override
      public void run( String[] args )
      {
          showIntroDialog();
          // After the dialog has gone, return keyboard
          // focus so keystrokes are not lost.
          frame.getRootPane().grabFocus();
          loop.run();
          System.out.println( solutionRecorder.getRecord() );
      }
  
      private static class AnswerHolder
      {
          public int answer;
      }
  
      /**
       * Must not be called from within event loop.
       */
      private int showDialog(
          final String title, 
          final Object message, 
          final Object[] options 
      )
      {
          final AnswerHolder holder = new AnswerHolder();
  
          runSwingCodeWithGameLoopBehind(
              new Runnable()
              {
                  @Override
                  public void run()
                  {
                      holder.answer = JOptionPane.showOptionDialog(
                          frame,
                          message,
                          title,
                          JOptionPane.YES_NO_OPTION,
                          JOptionPane.INFORMATION_MESSAGE,
                          null,
                          options,
                          options[ options.length - 1 ]
                      );
                  }
              }
          );
  
          return holder.answer;
      }
  
      /**
       * Must be called from within the event loop.
       */
      private boolean askExplodeAll()
      {
          MiniGameLoop bgDraw = new MiniGameLoop();
  
          new Thread( bgDraw ).start();
  
          try
          {
              String[] buttons = new String[] { t( "Cancel" ), t( "Explode!" ) };
  
              int ret = JOptionPane.showOptionDialog(
                  frame,
                  t( "Do you want to explode your rabbits?" ),
                  t( "Explode all rabbits?" ),
                  JOptionPane.YES_NO_OPTION,
                  JOptionPane.QUESTION_MESSAGE,
                  null,
                  buttons,
                  buttons[1]
              );
  
              return ( ret == 1 );
          }
          finally
          {
              bgDraw.running = false;
          }
      }
  
      private void runSwingCodeWithGameLoopBehind( Runnable doRun )
      {
          MiniGameLoop bgDraw = new MiniGameLoop();
  
          new Thread( bgDraw ).start();
  
          try
          {
              SwingUtilities.invokeAndWait( doRun );
          }
          catch ( InvocationTargetException e )
          {
              throw new RuntimeException( e );
          }
          catch ( InterruptedException e )
          {
              // Continue if interrupted
          }
          finally
          {
              bgDraw.running = false;
          }
      }
  
      /**
       * Not called from within event loop.
       */
      private void showIntroDialog()
      {
          if (inDemoMode())
          {
              return;
          }
  
          Util.Function<String, String> insertNewlines =
              new Util.Function<String, String>()
          {
              @Override
              public String apply( String inp )
              {
                  return Util.wrapToNewline( inp, DialogText.lineLength );
              }
          };
  
          showDialogs(
              world.name,
              Util.concat(
                  new Object[] { DialogText.introText( this.frame, world ) },
                  map(
                      insertNewlines,
                      world.hints,
                      new String[3]
                  )
              )
          );
      }
  
      private void showDialogs( String title, Object[] messages )
      {
          // Keep showing dialogs until we click start
          int retVal = 0;
          while ( retVal == 0 )
          {
              for ( int i = 0; i < messages.length; ++i )
              {
                  if ( "".equals( messages[i] ) )
                  {
                      // No more messages
                      break;
                  }
  
                  Object nextMessage = nextMessage( messages, i );
                  Object[] options = nextOptions( nextMessage, i );
                  retVal = showDialog(
                      title,
                      messages[i],
                      options
                  );
                  if ( options.length == 1 )
                  {
                      // There was only 1 option - we clicked Start
                      retVal = 1;
                  }
  
                  if ( retVal != 0 )
                  {
                      // We clicked Start or closed dialog
                      break;
                  }
              }
          }
      }
  
      private Object[] nextOptions( Object nextMessage, int i )
      {
          if ( "".equals( nextMessage ) )
          {
              if ( i == 0 )
              {
                  // There were no hints at all
                  return new Object[] { t( "Start" ) };
              }
              else
              {
                  // Go back to level description
                  return new Object[] { t( "Info" ), t( "Start" ) };
              }
          }
          else
          {
              // Another hint to come
              return new Object[] { t( hintName( i ) ), t( "Start" ) };
          }
      }
  
      private String hintName( int i )
      {
          if ( i == 0 )
          {
              return "Hint";
          }
          else
          {
              return "Hint " + ( i + 1 );
          }
      }
  
      private Object nextMessage( Object[] messages, int i )
      {
          if ( i >= messages.length - 1 )
          {
              return "";
          }
          else
          {
              return messages[ i + 1 ];
          }
      }
  
      /**
       * Not called from within event loop.
       */
      private void showWonDialog()
      {
          if (inDemoMode())
          {
              try
              {
                  Thread.sleep( 2000 );
              }
              catch ( InterruptedException e )
              {
                  // Ignore
              }
              return;
          }
  
          boolean levelSetComplete =
              (
                  menu != null &&
                  ( menuItem == menu.items[menu.items.length - 1] )
              );
  
          if ( levelSetComplete ) {
              Map<String, Object> values = DialogText.statsValues( world );
              values.put( "levelset", menu.name );
  
              showDialog(
                  t( "Level set complete!" ),
                  t(
                      "Saved: ${num_saved}  Needed: ${num_to_save}\n" +
                      "Well done! You have completed the ${levelset} levels!",
                      values
                  ),
                  new Object[]{ t( "Yippee!" ) }
              );
          }
          else
          {
              showDialog(
                  t( "You won!" ),
                  t(
                      "Saved: ${num_saved}  Needed: ${num_to_save}",
                      DialogText.statsValues( world )
                  ),
                  new Object[]{ t( "Ok" ) }
              );
          }
      }
  
      /**
       * Not called from within event loop.
       */
      private void showLostDialog()
      {
          if (inDemoMode())
          {
              try
              {
                  Thread.sleep( 2000 );
              }
              catch ( InterruptedException e )
              {
                  // Ignore
              }
              return;
          }
  
          showDialog(
              t( "You lost!" ),
              t(
                  "Saved: ${num_saved}  Needed: ${num_to_save}",
                  DialogText.statsValues( world )
              ),
              new Object[] { t( "Ok" ) }
          );
      }
  
      @Override
      public void showResult()
      {
          switch ( world.completionState() )
          {
              case WON:
              {
                  showWonDialog();
                  break;
              }
              case LOST:
              {
                  showLostDialog();
                  break;
              }
              default:
              {
                  // Maybe the user clicked back - do nothing here
              }
          }
  
          jframe.exit();
      }
  
      public void checkExplodeAll()
      {
          world.setPaused( true );
  
          boolean explode = askExplodeAll();
  
          world.setPaused( false );
  
          if ( explode )
          {
              world.changes.explodeAllRabbits();
          }
      }
  
      public int addToken( int tileX, int tileY, Token.Type ability )
      {
          int prev = world.abilities.get( ability );
          int now = physics.addToken( tileX, tileY, ability );
          if ( now != prev )
          {
              graphics.playSound( "place_token" );
          }
          if ( prev != now )
          {
              solutionRecorder.append( new PlaceTokenAction( tileX, tileY ) );
          }
          return now;
      }
  
      public Map<Token.Type, Integer> getAbilities()
      {
          return world.abilities;
      }
  
      public void addStatsChangedListener( Physics.StatsChangedListener listener )
      {
          physics.addStatsChangedListener( listener );
      }
  
      private boolean inDemoMode()
      {
          return ( !physics.solutionInterpreter.emptySteps );
      }
  }
  