
// MyGdxGame.java
// Andrew Davison, ad@coe.psu.ac.th, Jan 2023

/* A libgdx version of the Java SimplePaint application
   (and a program with a rather poor name).

   The sidebar GUI controls defined here are very like the ones
   defined in SimplePaint in its ControlsPanel class. 

   The menubar GUI controls defined here are new, and do 
   nothing to the painting on the canvas.

   Usage
   -----
   Test as a desktop app by calling:
    > gradlew desktop:run

   Can be compiled into an GDX 'applet' using:
    > gradlew html:superdev
      -- takes around 35 secs
      -- access in a browser at http://localhost:8080
    and
    > gradlew html:dist
      -- takes around 52 secs
      -- results are stored in test\html\build\dist
      -- then copied to a server, with index.html renamed to app.html

      -- my new index.html uses an iframe of size (800,600)
         when calling app.html, which creates a window of
         size (720, 480). The window size is specified in the
         GDX launcher code (in DesktopLauncher.java and 
         HtmlLauncher.java).
*/


package com.mygdx.game;


import com.badlogic.gdx.*;
import com.badlogic.gdx.Input.*;
import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.utils.viewport.*;
// import com.badlogic.gdx.math.MathUtils;

import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.graphics.g2d.*;
import com.badlogic.gdx.graphics.glutils.*;

import com.badlogic.gdx.scenes.scene2d.*;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.ui.Label.*;
import com.badlogic.gdx.scenes.scene2d.ui.TextField.*;
import com.badlogic.gdx.scenes.scene2d.utils.*;




public class MyGdxGame extends ApplicationAdapter 
{
  private static final float MIN_DELAY = 1/30f;

  private static final float CTRLS_FRAC = 0.14f;
      // fraction of the screen width used by the controls sidebar

  private static final float MENU_HEIGHT = 35f;

  private Skin skin;
  private Stage menuStage, drawStage, ctrlsStage;
  private Viewport menuViewport, drawViewport, ctrlsViewport;
      /* stages/viewports for the menubar, drawing area, and
         controls sidebar */

  private InputMultiplexer imp;

  private DrawCell dc;   // the drawing canvas

  // menubar widgets
  private SelectBox<String> selectBox;
  private CheckBox fancyCB;
  private Label fancyMenu;  //, helpMenu;
  private ImageButton helpMenu;
  private Dialog fancyDialog, helpDialog;
      /* A menu is a label (or button) and associated dialog.
         There are two menus used here: 'fancy' and 'help'
      */

  // globals used by the input textfield in the 'fancy' dialog
  private String tfInput;
  private boolean tfHasData = false;

  // controls sidebar widgets
  private CheckBox freeCB, ovalCB, rectCB, lineCB;
  private CheckBox filledCB;
  private CheckBox blackCB, redCB, greenCB, blueCB, whiteCB;
  private TextButton clearBut;



  public void create() 
  {
    Gdx.app.setLogLevel(Application.LOG_INFO);
    if (Gdx.app.getType() == Application.ApplicationType.WebGL)
      Gdx.app.log("MyGdxGame", "created");   // appears in browser web console

    skin = new Skin(Gdx.files.internal("metal/metal-ui.json"));
         // https://github.com/czyzby/gdx-skins/tree/master/metal
    // skin = new Skin(Gdx.files.internal("data/uiskin.json"));
               // skins are stored in test/assets/

    float winWidth = (float) Gdx.graphics.getWidth();
    float winHeight = (float) Gdx.graphics.getHeight();

    // -------------- stages and viewports ----------------

    // the drawing area is on the right, below the menubar
    drawViewport = new ExtendViewport(winWidth*(1f-CTRLS_FRAC), winHeight-MENU_HEIGHT); 
    drawStage = new Stage(drawViewport);
    dc = new DrawCell(winWidth*(1f-CTRLS_FRAC), winHeight-MENU_HEIGHT);  // the drawing canvas
    drawStage.addActor(dc);

    // the controls sidebar is on the left
    ctrlsViewport = new ExtendViewport(winWidth*CTRLS_FRAC, winHeight);        
    ctrlsStage = new Stage(ctrlsViewport);
    ctrlsStage.addActor(buildControls());   
          // buildControls() refers to dc, so create that object first

    // the menubar is on the right, at the top of the screen
    menuViewport = new ExtendViewport(winWidth*(1f-CTRLS_FRAC), MENU_HEIGHT); 
    menuStage = new Stage(menuViewport);
    menuStage.addActor(buildMenuBar());

    // ctrlsStage.setDebugAll(true);
    // menuStage.setDebugAll(true);
    // drawStage.setDebugAll(true);

    imp = new InputMultiplexer(menuStage, drawStage, ctrlsStage);
    Gdx.input.setInputProcessor(imp);


    // ------- mouse listeners for the drawing area ---------
    /* drawing is carried out by methods in the DrawCell class */

    drawStage.addListener(new InputListener() {
        public boolean touchDown(InputEvent event, 
                     float x, float y, int pointer, int button) 
        { if (!event.isHandled()) {   
            // only process this event if a dialog hasn't processed it
            dc.pressed(x, y);
            return true;
          }
          return false;
        }

        public void touchDragged(InputEvent event, 
                                 float x, float y, int pointer) 
        {  dc.dragged(x, y); }

        public void touchUp(InputEvent event, 
                      float x, float y, int pointer, int button) 
        {  dc.released(x, y);  }

    });
  }  // end of create()



  public void render() 
  {
    ScreenUtils.clear(Color.WHITE);

    int winWidth = Gdx.graphics.getWidth();
    int winHeight = Gdx.graphics.getHeight();
    float delta = Gdx.graphics.getDeltaTime();

    /* check for textfield input which is set by the input dialog
       in the 'Fancy' menu. */
    if (tfHasData) {
      // System.out.println("textfield input: " + tfInput);
      showMessage("Input", "Read: \"" + tfInput + "\"");
      tfHasData = false;
    }

    // the controls sidebar is on the left
    ctrlsViewport.update((int)Math.round(winWidth*CTRLS_FRAC), winHeight, true); 
	ctrlsStage.act( Math.min(delta, MIN_DELAY));
    ctrlsStage.draw(); 

    // the menubar is on the right, at the top of the screen
    menuViewport.update((int)Math.round(winWidth*(1f-CTRLS_FRAC)), 
                        (int)Math.round(MENU_HEIGHT), true);
    menuViewport.setScreenX((int)Math.round(winWidth*CTRLS_FRAC));  // move right
    menuViewport.setScreenY((int)Math.round(winHeight - MENU_HEIGHT));  // move up
    menuViewport.apply(true);
	menuStage.act( Math.min(delta, MIN_DELAY));
    menuStage.draw(); 

    // the drawing area is on the right, below the menubar
    drawViewport.update((int)Math.round(winWidth*(1f-CTRLS_FRAC)), 
                        (int)Math.round(winHeight - MENU_HEIGHT), true);  // height reduced
    drawViewport.setScreenX((int)Math.round(winWidth*CTRLS_FRAC));  // move right
    drawViewport.apply(true);
	drawStage.act( Math.min(delta, MIN_DELAY));
    drawStage.draw();          // drawing is carried out by the DrawCell actor

  }  // end of render()



  private Table buildMenuBar()
  // the menubar consists of a 'Fancy' and a 'Help' menu
  {
    buildFancyMenu();
    buildHelpMenu();

    // place menus in a horizontal table
    Table t = new Table(skin);
    t.setFillParent(true);
    t.setBackground(colorBackground(0.9f, 0.9f, 0.9f));  // gray
    t.align(Align.left);
    t.padTop(5);

    t.add(fancyMenu).width(60).pad(10);
    // t.add(helpMenu).width(60).pad(10);  // the label version of 'Help';
    t.add(helpMenu).size(30,30);           // the button version

    return t;
  }  // end of buildMenuBar()



  private void buildFancyMenu()
  /* The menu consists of a dialog window and a label that activates it.
     It is 'Fancy' since it looks nice but doesn't do
     anything to the painting in the drawing area.
  */
  {
    /* The dialog displays a table containing
       two labels, a popdown list, and a checkbox. 
       The first label activates an input box.
    */
    Label label1 = new Label("Input", skin);
    label1.addListener(new ClickListener(){
      public void clicked(InputEvent event, float x, float y) 
      {  inputBox("Input", "Type here: ");  }
    });

    Label label2 = new Label("Label", skin);
    label2.addListener(new ClickListener(){
      public void clicked(InputEvent event, float x, float y) 
      {  showMessage("Click Event", "Clicked on label");  }
    });

    selectBox = new SelectBox<String>(skin);
    selectBox.setItems("Item 1", "Item 2", "Item 3", "Item 4");
    selectBox.setScrollingDisabled(true);
    selectBox.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      {  showMessage("Change Event", "Selected: \"" + 
                                selectBox.getSelected() + "\"");
      }
    });

    fancyCB = new CheckBox("Fancy?", skin);
    fancyCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      {  showMessage("Change Event", "Fancy? " + 
                                    fancyCB.isChecked());
      }
    });

    // put the widgets into a table
    Table table = new Table();
    table.add(label1).align(Align.left).pad(5);
    table.row();
    table.add(label2).align(Align.left).pad(5);
    table.row();
    table.add(selectBox).width(100).fillX().pad(5);
    table.row();
    table.add(fancyCB).align(Align.left).pad(5);
    table.row();

    // place the table in a pop-down dialog
    fancyDialog = buildPopDown(table);


    // create the fancy menu label
    fancyMenu = new Label("Fancy", new LabelStyle(skin.get(LabelStyle.class)));
    fancyMenu.getStyle().background = colorBackground(0.85f, 0.85f, 0.85f);  // darker gray
    fancyMenu.setAlignment(Align.center);

    fancyMenu.addListener( new ClickListener(){
      public void clicked(InputEvent event, float x, float y) 
      // show the dialog directly underneath the menu label
      {  
        fancyDialog.show(drawStage);
        fancyDialog.setPosition(fancyMenu.getX(), 
                drawStage.getHeight() - fancyDialog.getHeight());
      }
    });
  }  // end of buildFancyMenu()



  private void buildHelpMenu()
  /* The menu consists of a dialog window and a ImageButton 
     (or a label) that activates it.
  */
  {
    /* The dialog displays text using a different font from the
       rest of the UI, in a region with a blue border.
    */
    BitmapFont labelFont = new BitmapFont(Gdx.files.internal("glassy/font-export.fnt"));
    // labelFont.getData().setScale(1.2f);
    LabelStyle ls = new LabelStyle(labelFont, Color.RED);

    Label msgLabel = new Label("\nSome not very helpful help text.\nThen some more.", ls);
    msgLabel.setAlignment(Align.left);
    msgLabel.setWrap(true);

    Table t = new Table();
    t.add(msgLabel).width(180).fillX().pad(5);
    helpDialog = buildPopDown(t);

    int w = (int)Math.round(helpDialog.getWidth());
    int h = (int)Math.round(helpDialog.getHeight());
    helpDialog.setBackground( borderBackground(Color.BLUE, w, h));

/*
    // help menu label
    helpMenu = new Label("Help", new LabelStyle(skin.get(LabelStyle.class)));
    helpMenu.getStyle().background = colorBackground(0.85f, 0.85f, 0.85f);  // darker gray
    helpMenu.setAlignment(Align.center);
*/
    // help menu image button
    helpMenu = new ImageButton( loadImage("query.png"));

    helpMenu.addListener( new ClickListener(){
      public void clicked(InputEvent event, float x, float y) 
      // show dialog directly underneath its menu label/button
      {  
        helpDialog.show(drawStage);
        helpDialog.setPosition(helpMenu.getX(), 
                drawStage.getHeight() - helpDialog.getHeight());
      }
    });
  }  // end of buildHelpMenu()



  private Dialog buildPopDown(Table contents)
  /* Use a Dialog to implement a popdown window that appears
     below a menu label/button. This dialog has no Ok button,
     but can be exited by pressing the ENTER, SPACE, or ESC keys,
     or by clicking outside the dialog area.
  */
  {
    final Dialog dialog = new Dialog("", skin);  // no title
    dialog.setBackground(colorBackground(0.85f, 0.85f, 0.85f));  // darker gray
    
    dialog.getContentTable().add(contents);

    // dialog.button("Ok", true).padBottom(5);
    dialog.key(Keys.ENTER, true);  
    dialog.key(Keys.SPACE, true);  
    dialog.key(Keys.ESCAPE, false);

    dialog.pack();
    dialog.setModal(true);
    dialog.setResizable(false);

    dialog.addListener( new InputListener() {
      public boolean touchDown(InputEvent event, 
                   float x, float y, int pointer, int button) 
      { if ((x < 0) || (x > dialog.getWidth()) || 
            (y < 0) || (y > dialog.getHeight())) {
          dialog.hide();   // user clicked outside the dialog area
        }
        return true;   // drawStage will not process this touch
      }
    });

    return dialog;
  }  // end of buildPopDown()



  private TextureRegionDrawable colorBackground(float r, float g, float b)
  // a pixel used for coloring backgrounds
  {
    Pixmap pixmap = new Pixmap(1,1, Pixmap.Format.RGBA8888);
    pixmap.setColor(r, g, b, 1f);  // (red,green,blue)
    pixmap.fill();
    TextureRegionDrawable bg = new TextureRegionDrawable( new Texture(pixmap));
    pixmap.dispose();
    return bg;
  }  // end of colorBackground()



  private TextureRegionDrawable borderBackground(Color color, int w, int h)
  // a (w x h) pixel map that is used for a background needing a border
  {
    Pixmap pixmap = new Pixmap(w, h, Pixmap.Format.RGBA8888);
    pixmap.setColor(color);
    pixmap.fillRectangle(0, 0, w, h);   // fill the area
    pixmap.setColor(Color.WHITE);
    pixmap.fillRectangle(2, 2, w-4, h-4);   // overwrite inside the area
    TextureRegionDrawable bg = new TextureRegionDrawable( new Texture(pixmap));

    pixmap.dispose();
    return bg;
  }  // end of borderBackground()


  private TextureRegionDrawable loadImage(String fnm)
  {  return new TextureRegionDrawable( new Texture( Gdx.files.internal(fnm)));  } 


  private void showMessage(String title, String msg)
  // display a message in a dialog box
  {
    Dialog dialog = new Dialog(title, skin); 

    Label label = new Label(msg, skin);
    label.setAlignment(Align.center);
    label.setWrap(true);
    dialog.getContentTable().add(label).width(130).pad(5);
    dialog.button("Ok", true).padBottom(5);

    dialog.pack();
    dialog.setModal(true);
    dialog.setResizable(false);
    dialog.addListener( new InputListener() {
      public boolean touchDown(InputEvent event, 
               float x, float y, int pointer, int button) 
      { return true;  }    // drawStage will not process this touch
    });

    dialog.show(drawStage);
  }  // end of showMessage()


  private void inputBox(String title, String prompt)
  /* Read user input via a textfield in a dialog box. 
     The data is stored in the global tfInput, and tfHasData is
     set to true. The data is accessed at the start of render().
  */
  {
    Dialog dialog = new Dialog(title, skin) {
      public void result(Object obj) 
      {
        tfHasData = (Boolean) obj;
        if (!tfHasData)   // "Cancel" was pressed, so false
          tfInput = null;
       }
    }; 

    Table table = new Table();
    table.align(Align.top);
    table.pad(5);

    Label label = new Label(prompt,skin);
    table.add(label).align(Align.left).padBottom(5);

    TextField tf = new TextField("", skin);
    tf.setTextFieldListener(new TextFieldListener() {
       public void keyTyped(TextField textField, char key) 
       // keep updating tfInput as keys are typed
       {  tfInput = textField.getText();
          // System.out.println("text field: \"" + tfInput + "\"");
       }
    });

    table.add(tf).width(80).align(Align.left).padBottom(5);
    table.row();

    dialog.getContentTable().add(table).width(200).pad(5);
    dialog.button("Ok", true).padBottom(5);
    dialog.button("Cancel", false).padBottom(5);
      // Dialog.result() will be called with these boolean values

    dialog.pack();
    dialog.setModal(true);
    dialog.setResizable(false);
    dialog.addListener( new InputListener() {
      public boolean touchDown(InputEvent event, 
               float x, float y, int pointer, int button) 
      {  return true;  }   // drawStage will not process this touch
    });

    dialog.show(drawStage);
  }  // end of inputBox()



  private Table buildControls()
  /* Choose a shape (freehand, oval, rectangle, or line),
     filled or not, a color (black, red, green, or white),
     and a "Clear" button.

     These controls send their data to the DrawCell object, dc,
     where the drawing is carried out.
  */
  {
    // background colors for areas in the sidebar
    TextureRegionDrawable grayBG = colorBackground(0.9f, 0.9f, 0.9f);  // gray
    TextureRegionDrawable lgrayBG = colorBackground(0.95f, 0.95f, 0.95f);  // light gray

    Table ctrlsTable = new Table();
    ctrlsTable.setFillParent(true);
    ctrlsTable.setBackground(grayBG);
    ctrlsTable.align(Align.top | Align.left);
    ctrlsTable.padTop(5);


    // ------------------ shape checkboxes ----------------------
    // all the shape controls are together in their own subtable

    Table t1 = new Table();
    t1.setBackground(lgrayBG);
    t1.align(Align.top);
    t1.pad(5);

    ctrlsTable.add(t1);
    ctrlsTable.row();

    Label shapeLabel = new Label("Shape:",skin);
    t1.add(shapeLabel).align(Align.left).padBottom(5);
    t1.row();

    // four checkboxes, one of which can be on at a time...

    freeCB = new CheckBox("Freehand", skin);
    t1.add(freeCB).align(Align.left).padBottom(5);
    t1.row();
    freeCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      { if (freeCB.isChecked())
          dc.setShape(DrawCell.FREEHAND);
      }
    });

    ovalCB = new CheckBox("Oval", skin);
    t1.add(ovalCB).align(Align.left).padBottom(5);
    t1.row();
    ovalCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      { if (ovalCB.isChecked())
          dc.setShape(DrawCell.OVAL);
      }
    });

    rectCB = new CheckBox("Rectangle", skin);
    t1.add(rectCB).align(Align.left).padBottom(5);
    t1.row();
    rectCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      { if (rectCB.isChecked())
          dc.setShape(DrawCell.RECTANGLE);
      }
    });

    lineCB = new CheckBox("Line", skin);
    t1.add(lineCB).align(Align.left).padBottom(5);
    t1.row();
    lineCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      { if (lineCB.isChecked())
          dc.setShape(DrawCell.LINE);
      }
    });

    ButtonGroup shapesBG = 
         new ButtonGroup(freeCB, ovalCB, rectCB, lineCB);
    shapesBG.setMaxCheckCount(1);   // only one selected at a time
    shapesBG.setMinCheckCount(0);
    shapesBG.setChecked("Freehand");  // start with freehand


    // ------------------ filled checkbox ----------------------

    filledCB = new CheckBox("Filled?", skin);
    ctrlsTable.add(filledCB).align(Align.left).pad(5);
    ctrlsTable.row();
    filledCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      {  dc.setFilled(filledCB.isChecked());  }
    });


    // ------------------ color checkboxes ----------------------
    // all the color controls are together in their own subtable

    Table t2 = new Table();
    t2.setBackground(lgrayBG);
    t2.align(Align.top);
    t2.pad(5);

    ctrlsTable.add(t2).align(Align.left);
    ctrlsTable.row();

    Label colorsLabel = new Label("Colors:",skin);
    t2.add(colorsLabel).align(Align.left).padBottom(5);
    t2.row();

    // four checkboxes, one of which can be on at a time...

    blackCB = new CheckBox("Black", skin);
    t2.add(blackCB).align(Align.left).padBottom(5);
    t2.row();
    blackCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      { if (blackCB.isChecked())
          dc.setDrawColor(Color.BLACK);
      }
    });

    redCB = new CheckBox("Red", skin);
    t2.add(redCB).align(Align.left).padBottom(5);
    t2.row();
    redCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      { if (redCB.isChecked())
          dc.setDrawColor(Color.RED);
      }
    });

    greenCB = new CheckBox("Green", skin);
    t2.add(greenCB).align(Align.left).padBottom(5);
    t2.row();
    greenCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      { if (greenCB.isChecked())
          dc.setDrawColor(Color.GREEN);
      }
    });

    blueCB = new CheckBox("Blue", skin);
    t2.add(blueCB).align(Align.left).padBottom(5);
    t2.row();
    blueCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      { if (blueCB.isChecked())
          dc.setDrawColor(Color.BLUE);
      }
    });

    whiteCB = new CheckBox("White", skin);
    t2.add(whiteCB).align(Align.left).padBottom(5);
    t2.row();
    whiteCB.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      { if (whiteCB.isChecked())
          dc.setDrawColor(Color.WHITE);
      }
    });

    ButtonGroup colorsBG = 
         new ButtonGroup(blackCB, redCB, greenCB, blueCB, whiteCB);
    colorsBG.setMaxCheckCount(1);  // only one selected at a time
    colorsBG.setMinCheckCount(0);
    colorsBG.setChecked("Black");  // start with black selected


    // ------------------ clear button ----------------------

    clearBut = new TextButton("Clear", skin);
    ctrlsTable.add(clearBut).align(Align.left).padTop(5).padLeft(5).padBottom(5);
    ctrlsTable.row();
    clearBut.addListener(new ChangeListener() {
      public void changed(ChangeEvent event, Actor actor) 
      {  dc.clear(); }
    });

    return ctrlsTable;
  }  // end of buildControls()


/*
  public void resize (int width, int height) 
  // no resizing is possible
  { }
*/


  public void dispose () 
  {
    menuStage.dispose();
    drawStage.dispose();
    ctrlsStage.dispose();
    dc.dispose();
    skin.dispose();
  }

}  // end of MyGdxGame class
