
// BricksManager.java
// Andrew Davison, April 2005, ad@fivedots.coe.psu.ac.th

/* Načítá a spravuje cihličkovou síť.
   Řeší také nejrůznější detekce kolizí objektu typu JumperSprite.

   Cihličkovou síť čte z konfiguračního souboru a s její pomocí
   vytváří seznam typu ArrayList objektů typu Bick, který nese
   název bricksList.

   Kolekce cihliček definuje cihličkovou síť, který se pohybuje a 
   vykresluje stejným způsobem jako pásmo obrázků v objektu typu Ribbon.
   
   Krok pohybu cihličkové sítě je pevně dát hodnotou moveSize, 
   která je definována jako část šířky obrázku cihličky.

   ----
   Objekt typu JumperSprite používá třídu BricksManager pro detekci kolizí.
   Při stoupání/klesání musí svůj pohyb zastavit, pokud by měl vstoupit
   do cihličky. K tomuto účelu využívá metody checkBirckBase() a checkBrichTop().

   Když se objekt typu JumperSprite pohybuje doleva/doprava, musí nejdříve ověřit,
   nevstoupí-li do nějaké cihličky. K tomu mu slouží metoda insideBrick().

   Když se objekt typu JumperSprite pohybuje, může propadnou do volného prostoru -
   pod ním již není žádná cihlička. Tento stav testuje také metoda insideBrick().
*/

import java.awt.*;
import java.awt.image.*;
import java.util.*;
import java.io.*;



public class BricksManager
{
  private final static String IMAGE_DIR = "Images/";
  private final static int MAX_BRICKS_LINES = 15;
     // maximální počet řádků cihliček ve scéně

  private final static double MOVE_FACTOR = 0.25;  
     // upravuje rychlost pohybu cyhličkové sítě, menší hodnota --> pomalejší pohyb

  private int pWidth, pHeight;    // rozměry zobrazovacího panelu
  private int width, height;      // maximální rozměry cihličkové sítě
                                  // width > pWidth

  private int imWidth, imHeight;  // rozměry obrázku cihličky
  private int numCols, numRows;   
      // počet cihliček ve směru os x a y

  private int moveSize;       
      // velikost pohybu sítě při každém tiknutí (v pixelech)
      // moveSize == šířka obrázku cihličky * MOVE_FACTOR
      /* pásy a 'jack' používají stejnou hodnotu moveSize. */

  private boolean isMovingRight;  // příznaky pohybu
  private boolean isMovingLeft;

  private int xMapHead;    
     /* x-voá souřadnice v panelu, odkud se má začít kreslit
        cihličková síť (její čelo).
        Může být v rozsahu od -width do with (vyjma), takže 
        může mít hodnotu mimo hranicí panelu (0-pWidth).

        Jak se její hodnota mění, bude se na obrazovce obvykle
        objevovat kombinace jejího týlu následovaná čelem.

        xMapHead hraje stejnou roli jako xImHead v objektu sítě.
     */


  private ArrayList bricksList;   
    // uchovává objekty, které tvoří cihličkovou síť

  private ArrayList[] columnBricks;
    // Objekty typu Brick uspořádané dle sloupců
    // (rychleji se prohledává, než v případě seznamu bricksList)

  private ImagesLoader imsLoader;
  private ArrayList brickImages = null;    
         // uchovává všechny obrázky načtené objektem imsLoader
 

  public BricksManager(int w, int h, String fnm, ImagesLoader il)
  {
    pWidth = w; pHeight = h;
    imsLoader = il;

    bricksList = new ArrayList();
    loadBricksFile(fnm);
    initBricksInfo();
    createColumns();

    moveSize = (int)(imWidth * MOVE_FACTOR);
    if (moveSize == 0) {
      System.out.println("moveSize cannot be 0, setting it to 1");
      moveSize = 1;
    }

    isMovingRight = false;   // žádný pohyb at start
    isMovingLeft = false;
    xMapHead = 0;
  }  // konec metody: BricksManager()


  // ----------- načítání údajů o cihličkách -------------------

  private void loadBricksFile(String fnm)
  /* Načte cihličkovou síť z konfiguračního souboru fnm.
     Síť začíná obrázkovým pásmem, které obsahuje obrázky odkazované
     v síti. Formát:
        s <fnm> <number> 


     To znamená, že síť může používat obrázky s čísly 0 až 
     <number-1>. Předpokládáme, že number je měnší jak 10.

     Následuje síť cihliček. Každý řádek se zpracovává
     metodou storeBricks(). Může zde být nejvýše MAX_BRICKS_LINES
     řádků.

     Konfigurační soubor může obsahovat prázdé řádky a řádky s komentáři
     (začínají //), které se ignorují.
  */
  { 
    String imsFNm = IMAGE_DIR + fnm;
    System.out.println("Reading bricks file: " + imsFNm);

    int numStripImages = -1;
    int numBricksLines = 0;
    try {
      BufferedReader br = new BufferedReader( new FileReader(imsFNm));
      String line;
      char ch;
      while((line = br.readLine()) != null) {
        if (line.length() == 0)  // ignore a prázdný řádek
          continue;
        if (line.startsWith("//"))   // ignoruj řádek s komentářem
          continue;
        ch = Character.toLowerCase( line.charAt(0) );
        if (ch == 's')  // obrázkové pásmo
          numStripImages = getStripImages(line);
        else {  // řádek se sítí cihliček
          if (numBricksLines > MAX_BRICKS_LINES) 
            System.out.println("Dosaženo maxima, přeskakuji řádek s cihličkami: " + line);
          else if (numStripImages == -1) 
            System.out.println("Není definováno pásmo, přeskakuji řádek s cihličkami: " + line);
          else {
            storeBricks(line, numBricksLines, numStripImages);
            numBricksLines++;
          }
        }
      }
      br.close();
    } 
    catch (IOException e) 
    { System.out.println("Chyba při čtení souboru: " + imsFNm);
      System.exit(1);
    }
  }  // konec metody: loadBricksFile()


  private int getStripImages(String line)
  /* formát:
        s <fnm> <number>

     Pásmové obrázky se používají pro reprezentaci cihliček
     při jejich vykreslování. Číslo v síti cihliček je mapováno
     na obrázek na příslušné pozici v pásmu obrázků.

     Pásmové obrázky se načítají pomocí objektu typu ImagesLoader a poté
     se umístí do pole bricksImages[].
  */
  { StringTokenizer tokens = new StringTokenizer(line);
    if (tokens.countTokens() != 3) {
      System.out.println("Nesprávný počet argumentů pro " + line);
      return -1;
    }
    else {
      tokens.nextToken();    // přeskoč označení příkazu
      System.out.print("Pásmo cihliček: ");

      String fnm = tokens.nextToken();
      int number = -1;
      try {
        number = Integer.parseInt( tokens.nextToken() );
        imsLoader.loadStripImages(fnm, number);   // ulož obrázek s pásmem
        brickImages = imsLoader.getImages( getPrefix(fnm) ); 
            // ulož všechny obrázky do globálního pole
      }
      catch(Exception e)
      { System.out.println("Číslo je nepsrávné na řádce " + line);  }

      return number;
    }
  }  // konec metody: getStripImages()


  private String getPrefix(String fnm)
  // extrahuje jméno souboru před '.'
  {
    int posn;
    if ((posn = fnm.lastIndexOf(".")) == -1) {
      System.out.println("Pro dané jméno souboru nebyl nalezen žádný prefix: " + fnm);
      return fnm;
    }
    else
      return fnm.substring(0, posn);
  } // konec metody: getPrefix()



  private void storeBricks(String line, int lineNo, int numImages)
  /* Přečte jediný řádek cihliček a vytvoří objekt typu Brick.
     Řádek obsahuje číslice a mezery (které se ignorují). Každá
     číslice se stane jedním objektem typu Brick.
     Kolekce objektů typu Brick se uloží do seznamu typu ArrayList
     s názvem bricksList.
  */
  {
    int imageID;
    for(int x=0; x < line.length(); x++) {
      char ch = line.charAt(x);
      if (ch == ' ')   // mezery ignorujeme
        continue;
      if (Character.isDigit(ch)) {
        imageID = ch - '0';    // předpokládáme, že číslice je 0-9
        if (imageID >= numImages)
          System.out.println("ID obrázku" + imageID + " je mimo rozsah");
        else   // vytvoř nový objekt typu Brick
          bricksList.add( new Brick(imageID, x, lineNo) );
      }
      else
        System.out.println("Znak cihličky  " + ch + " není číslice");
    }
  }  // konec metody: storeBricks()


  // --------------- inicializace datových struktur -----------------

  private void initBricksInfo()
  /* Z kolekci cihliček v seznamu bricksList se extrahují
     nejrůznější globální data a kontroluje se, jsou-li
     splněny jistá kriteria (např. je-li maximální šířka cihličky
     větší než šířka panelu (width >= pWidth).
     Každému objektu typu Brick se také přiřadí jeho obrázek.
  */
  {
    if (brickImages == null) {
      System.out.println("Nebyly načteny žádné obrázky cihliček");
      System.exit(1);
    }
    if (bricksList.size() == 0) {
      System.out.println("Nebyla načtena síť cihliček");
      System.exit(1);
    }

    // ulož rozměry obrázku cihličky (předpokládáme, že je u všech stejný)
    BufferedImage im = (BufferedImage) brickImages.get(0);
    imWidth = im.getWidth();
    imHeight = im.getHeight(); 

    findNumBricks();
    calcMapDimensions();
    checkForGaps();

    addBrickDetails();
  }  // konec metody: initBricksInfo();

  
  private void findNumBricks()
  // najde maximální počet cihliček ve směru os x a y
  {
    Brick b;
    numCols = 0;
    numRows = 0;
    for (int i=0; i < bricksList.size(); i++) {
      b = (Brick) bricksList.get(i);
      if (numCols < b.getMapX())
        numCols = b.getMapX();
      if (numRows < b.getMapY())
        numRows = b.getMapY();
    }
    numCols++;    // protože chceme maximální počet, ne maximální  souřadnici
    numRows++;
  }  // konec metody: findNumBricks()


  private void calcMapDimensions()
  // převede maximální počet cihliček na maximální rozměry v pixelech
  {
    width = imWidth * numCols;
    height = imHeight * numRows;

    // ukonči program, není-li šířka sítě cihliček větší jak šířka panelu
    if (width < pWidth) {
      System.out.println("Šířka sítě cihliček je menší než šířka panelu");
      System.exit(0);
    }
  }  // konec metody: calcmapDimensions()


  private void checkForGaps()
  /* Ověří, že spodní řádek sítě (numRows-1) má cihličku na každé
     x-ové pozici od 0 do numCols-1.
     Tím zabrázníme 'jackovi', aby spadl dírou dolů pod obrazku.
  */
  {
    boolean[] hasBrick = new boolean[numCols];
    for(int j=0; j < numCols; j++)
      hasBrick[j] = false;

    Brick b;
    for (int i=0; i < bricksList.size(); i++) {
      b = (Brick) bricksList.get(i);
      if (b.getMapY() == numRows-1)
        hasBrick[b.getMapX()] = true;   
    }

    for(int j=0; j < numCols; j++)
      if (!hasBrick[j]) {
        System.out.println("Ve spodním řádku cihličkové sítě je díra na pozici " + j);
        System.exit(0);
      }
  }  // konec metody: checkForGaps()


  private void addBrickDetails()
  /* Přidá k cihličkám odkazy na obrázky a vypočítá jejich y-ové souřadnice
     uvnitř sítě cihliček. */
  {
    Brick b;
    BufferedImage im;
    for (int i=0; i < bricksList.size(); i++) {
      b = (Brick) bricksList.get(i);
      im = (BufferedImage) brickImages.get( b.getImageID());
      b.setImage(im);
      b.setLocY(pHeight, numRows);
    }
  }  // konec metody: addBrickDetails()



  private void createColumns()
  // uloží údaje o cihličkách dle příslušného sloupce pro rychlejší vyhledávání/přístup
  {
    columnBricks = new ArrayList[numCols];
    for (int i=0; i < numCols; i++)
      columnBricks[i] = new ArrayList();

    Brick b;
    for (int j=0; j < bricksList.size(); j++) {
      b = (Brick) bricksList.get(j);
      columnBricks[ b.getMapX() ].add(b);    // cihličky nejsou nijak seřazované
    }
  }  // konec metody: createColumns()


  // ---------------------- pohyb cihličkové sítě ---------------

  public void moveRight()
  { isMovingRight = true;
    isMovingLeft = false;
  }  // konec metody: moveLeft()


  public void moveLeft()
  { isMovingRight = false;
    isMovingLeft = true;
  }

  public void stayStill()
  { isMovingRight = false;
    isMovingLeft = false;
  }

  
  public void update()
  /* Zvýší hodnotu xMapHead v závislosti na příznaku pohybu.
     Její rozsah je omezen od -width do width (včetně), kde width
     je šířka obrázku.
  */
  {
    if (isMovingRight)
      xMapHead = (xMapHead + moveSize) % width;
    else if (isMovingLeft)
      xMapHead = (xMapHead - moveSize) % width;

    // System.out.println("xMapHead je " + xMapHead);

  } // konec metody: updateBricks()



  // -------------- vykreslení cihliček ----------------------


  public void display(Graphics g)
  /*  Síť cihliček (SC) je širší než panel (width >= pWidth)
     Uvažujeme 4 připady:
       - xMapHead >= 0: vykreslí týl SC a čelo SC, nebo jen týl SC
       - xMapHead < 0: vykreslí týl SC nebo týl SC a čelo SC

     Proměnná xMapHead je v rozsahu -width až width (vyjma)
  */
  {
    int bCoord = (int)(xMapHead/imWidth) * imWidth;
      // bCoord je x-ová souřadnice pro vykreslení cihličky obsahující xMapHead
    int offset;    // offset je vzdálenost bCoord od xMapHead
    if (bCoord >= 0)
      offset = xMapHead - bCoord;   // offset je kladný
    else  // negative position
      offset = bCoord - xMapHead;   // offset je kladný
    // System.out.println("bCoord: " + bCoord + ", offset: " + offset);


    if ((bCoord >= 0) && (bCoord < pWidth)) {  
      drawBricks(g, 0-(imWidth-offset), xMapHead, width-bCoord-imWidth);   // týl SC
      drawBricks(g, xMapHead, pWidth, 0);  // čelo SC
    }
    else if (bCoord >= pWidth)
      drawBricks(g, 0-(imWidth-offset), pWidth, width-bCoord-imWidth);  // týl SC
    else if ((bCoord < 0) && (bCoord >= pWidth-width+imWidth))
      drawBricks(g, 0-offset, pWidth, -bCoord);		// týl SC
    else if (bCoord < pWidth-width+imWidth) { 
      drawBricks(g, 0-offset, width+xMapHead, -bCoord);     // týl SC
      drawBricks(g, width+xMapHead, pWidth, 0);     // čelo SC
    } 
  } // konec metody: display()


  private void drawBricks(Graphics g, int xStart, int xEnd, int xBrick)
  /* Vykreslí cihličky do panelu JPanel počínaje xStart a konče xEnd.
     Cihličky vykresluje po sloupcích oddělených imWidth pixely.

     První vykreslený sloupec cihliček odpovídá lokaci xBrick v síti cihliček.
  */
  { int xMap = xBrick/imWidth;   // vezmi pozici sloupce cihličky
                                 // v cihličkové síti
    // System.out.println("xStart: " + xStart + "; xEnd: " + xEnd);
    // System.out.println("xBrick: " + xBrick + "; xMap: " + xMap);
    ArrayList column;
    Brick b;
    for (int x = xStart; x < xEnd; x += imWidth) {
      column = columnBricks[ xMap ];   // získej aktuální sloupec
      for (int i=0; i < column.size(); i++) {   // vykresli všechny cihličky
         b = (Brick) column.get(i);
         b.display(g, x);   // vykresli cihličku b na panel do pozice x
      }
      xMap++;  // jdi na následující sloupec cihliček
    }
  }  // konec metody: drawBricks()



  // ----------------- metody vztahující se k objektu typu JumperSprite -------------
  // nejrůznější formy detekce kolize s cihličkami


  public int getBrickHeight()
  {  return imHeight; }


  public int findFloor(int xSprite)
  /* Volá se při inicializaci spritu pro nalezení cihličky
     obsahující lokaci xSprite, která je mezi ostatními
     ve stejném sloupci nejvýše.
     Vrátí y-ovou pozici cihličky.

     Parametr xSprite udává jak souřadnici v panelu tak i 
     uvnitř sítě cihliček, protože síť se dosud nepohnula.

     Parametr xSprite se nejdříve převede na x-ový index do
     sítě cihliček a tato hodnota se použije pro vyhledání 
     relevantního sloupce pro nalezení maximální y-ové lokace.

     Vrácená y-ová lokace je povrchem cihličky, na nemž
     bude na začátku sprite spočívat.
  */
  {
    int xMap = (int)(xSprite/imWidth);   // x-ový index v síti

    int locY = pHeight;    // počáteční y-ová pozice (největší možná)
    ArrayList column = columnBricks[ xMap ];

    Brick b;
    for (int i=0; i < column.size(); i++) {
      b = (Brick) column.get(i);
      if (b.getLocY() < locY)
        locY = b.getLocY();   // sniž locY (i.e. posuň vzhůru)
    }
    return locY;
  }  // konec metody: findFloor()



  public int getMoveSize()
  {  return moveSize;  }


  public boolean insideBrick(int xWorld, int yWorld)
  /* zkontroluje, není-li bod v souřadnicích herního světa uvnitř cihličky. */
  {
    Point mapCoord = worldToMap(xWorld, yWorld);
    ArrayList column = columnBricks[ mapCoord.x ];

    Brick b;
    for (int i=0; i < column.size(); i++) {
      b = (Brick) column.get(i);
      if (mapCoord.y == b.getMapY())
        return true;
    }
    return false;
  }  // konec metody: insideBrick()


  private Point worldToMap(int xWorld, int yWorld)
  // převeď (x,y) ze souřadnic světa na n-tici indexů v síti
  {
    // System.out.println("World: " + xWorld + ", " + yWorld);

    xWorld = xWorld % width;   // omez na interval (-width až width)
    if (xWorld < 0)            // uprav na kladné číslo
      xWorld += width;
    int mapX = (int) (xWorld/imWidth);   // x-ový index v síti

    yWorld = yWorld - (pHeight-height);  // relativně k síti
    int mapY = (int) (yWorld/imHeight);  // y-ový index v síti

    if (yWorld < 0)   // nad horní hranicí cihličky
      mapY = mapY-1;  // uprav na následující horní 'řadu'

    // System.out.println("Map: " + mapX + ", " + mapY);
    return new Point(mapX, mapY);
  }  // konec metody: worldToMap()

  
  public int checkBrickBase(int xWorld, int yWorld, int step)
  /* Sprite se pohybuje vzhůru. Tato metoda zkontroluje jeho následující
     polohu (xWorld, yWorld), nevstupuje-li ze spodu do cihličky.

     Pokud ano, pak velikost jeho kroku sníží na smallStep,
     takže se jen dotkne podstavy cihličky.
  */
  {
    if (insideBrick(xWorld, yWorld)) {
      int yMapWorld = yWorld - (pHeight-height);
      int mapY = (int) (yMapWorld/imHeight);  // y-ový index v síti
      int topOffset = yMapWorld - (mapY * imHeight);
      int smallStep = step - (imHeight-topOffset);
      // System.out.println("base smallStep: " + smallStep);
      return smallStep;
    }
    return step;   // beze změmy
  }  // konec metody: checkBrickBase()


  public int checkBrickTop(int xWorld, int yWorld, int step)
  /* Sprite se pohybuje dolů. Tato metoda zkontroluje jeho následující
     polohu (xWorld, yWorld), nevstupuje-li do cihličky shora.

     IPokud ano, pak velikost jeho kroku sníží na smallStep,
     takže se jen dotkne horní části cihličky.
  */
  {
    if (insideBrick(xWorld, yWorld)) {
      int yMapWorld = yWorld - (pHeight-height);
      int mapY = (int) (yMapWorld/imHeight);  // y-ový index v síti
      int topOffset = yMapWorld - (mapY * imHeight);
      int smallStep = step - topOffset;
      // System.out.println("top smallStep: " + smallStep);
      return smallStep;
    }
    return step;   // beze změmy
  }  // konec metody: checkBrickTop()

}  // konec třídy: BricksManager
