package org.homebrew;

import java.awt.*;
import java.util.Random;
import javax.media.Manager;
import javax.media.Player;
import javax.tv.xlet.Xlet;
import javax.tv.xlet.XletContext;
import org.bluray.net.BDLocator;
import org.bluray.ui.event.HRcEvent;
import org.davic.media.MediaLocator;
import org.dvb.event.EventManager;
import org.dvb.event.UserEvent;
import org.dvb.event.UserEventListener;
import org.dvb.event.UserEventRepository;
import org.havi.ui.HScene;
import org.havi.ui.HSceneFactory;
import org.homebrew.engine.ImageEntity;
import org.homebrew.engine.Point2D;
import org.homebrew.engine.SoundClip;
import org.homebrew.engine.Sprite;
import org.homebrew.engine.SaveData;

/**
 * The main game application. This class is responsible for displaying and updating of all the game
 * entities as well as handling user input.
 */
public class MyXlet extends Container implements Xlet, Runnable, UserEventListener {

    /** constant for the screen width */
    final int SCREENWIDTH = 1280;

    /** constant for the screen height */
    final int SCREENHEIGHT = 720;

    /** constant for the x coordinate of the center of the screen */
    final int SCREENCENTERX = SCREENWIDTH / 2;

    /** constant for the y coordinate of the center of the screen */
    final int SCREENCENTERY = SCREENHEIGHT / 2;

    /** constant for games' desired frame per seconds */
    final int FPS = 24;

    /** constant for bird's max up speed */
    final int UPFORCEY = -24;

    /** constant for pipe velocity */
    final int PIPEXVELOCITY = -12;

    /** gravity */
    final int GRAVITY = 4;

    /** constant for y point to clip bird to top of screen */
    final int BIRDTOPBOUNDRY = 0;

    /** constant for y point to determine if bird has died */
    final int BIRDDEATHBOUNDRY = 520;

    /** max number of pipe sprites */
    private final int MAXPIPESPRITES = 1;

    /** scene to draw on */
    private HScene scene;

    /** scene font for text display */
    private Font font;

    /** thread for the game loop */
    private Thread gameloop;

    /** image for double buffering */
    private Image backbuffer;

    /** game state */
    int state;

    /** players score */
    int score;

    /** players hi score */
    int hiscore;

    /** random number generator */
    Random rand;

    /** game background */
    private ImageEntity background;

    /** player sprite */
    private Sprite bird;

    /** top pipe sprite */
    private Sprite[] topPipe;

    /** bottom pipe sprite */
    private Sprite[] bottomPipe;

    /** sample for wing flap */
    private SoundClip wingFlap;

    /** sample for point */
    private SoundClip point;

    /** sample for hit */
    private SoundClip hit;

    /** sample for death */
    private SoundClip death;

    /** stores high score in ADA */
    SaveData highscoredata;

    /**
     * initializes the xlet, starts the background music, creates the double buffer, initializes all the
     * game objects and sets up the input handlers
     * @param context xlet context
     */
    public void initXlet(XletContext context) {
        setSize(SCREENWIDTH, SCREENHEIGHT);
        scene = HSceneFactory.getInstance().getDefaultHScene();
        scene.add(this);
        scene.validate();

        try
        {
            MediaLocator locator = new MediaLocator(new BDLocator(null, -1, 0));
            Player videoplayer = Manager.createPlayer(locator);
            videoplayer.start();
        }
        catch (Exception e) {
            System.err.println("Couldn't load video: " + e.toString());
        }

        // create the back buffer for smooth graphics
         Dimension d = getSize();
         if (backbuffer == null || backbuffer.getWidth(null) != d.width
         || backbuffer.getHeight(null) != d.height)
         {
             backbuffer = createImage(d.width, d.height);
         }

         // seed the random number generator
        rand = new Random();

        // setup the background
        background = new ImageEntity(scene);
        background.load("/Data/Images/background.png");
        background.setY(576);

        // setup the player sprite
        bird = new Sprite(scene);
        bird.load("/Data/Images/FlappyBird.png");

        // load the top pipe sprite
        topPipe = new Sprite[MAXPIPESPRITES];

        // load the bottom pipe sprite
        bottomPipe = new Sprite[MAXPIPESPRITES];

        for(int i = 0; i < MAXPIPESPRITES; i++) {
            // initialize the top pipe sprites
            topPipe[i] = new Sprite(scene);
            topPipe[i].load("/Data/Images/TopPipe.png");

            // initialize the bottom pipe sprites
            bottomPipe[i] = new Sprite(scene);
            bottomPipe[i].load("/Data/Images/BottomPipe.png");
        }

        // load the wing sound effect
        wingFlap = new SoundClip("/Data/Sounds/flap.pcm");

        // load the point sound effect
        point = new SoundClip("/Data/Sounds/point.pcm");

        // load the hit sound effect
        hit = new SoundClip("/Data/Sounds/hit.pcm");

        // load the death sound effect
        death = new SoundClip("/Data/Sounds/death.pcm");

        try {
            highscoredata = new SaveData(context, "user-hiscore");
        } catch (Exception e) {
            System.err.println("failed to institate savedata");
        }

        // if high score data exists
        if (highscoredata.exists()) {
            hiscore = highscoredata.getInteger();
            System.err.println("high score: " + highscoredata.getInteger());
        }
        else {
            hiscore = 0;
            System.err.println("no high score");
        }

        // add input event
        UserEventRepository userEventRepo = new UserEventRepository("input");

        // add event listener for enter key
        userEventRepo.addKey(HRcEvent.VK_ENTER);

        // finally attach the event to the current Xlet instance
        EventManager.getInstance().addUserEventListener(this, userEventRepo);
    }

    /**
     * makes the scene visible, starts the game loop and calls repaint to draw the first frame
     */
    public void startXlet() {
        setVisible(true);
        scene.setVisible(true);
        gameloop = new Thread(this);
        gameloop.start();
        state = GameState.TITLESCREEN;
        repaint();
    }

    /**
     * hides the scene
     */
    public void pauseXlet() {
        setVisible(false);
    }

    /**
     * returns the memory used by the xlet and terminates the thread used for the game loop
     * @param unconditional terminate conditionally if true, otherwise false
     */
    public void destroyXlet(boolean unconditional) {
        scene.remove(this);
        scene = null;
        gameloop = null;
    }

    /**
     * main game loop
     */
    public void run() {
        //acquire the current thread
        Thread t = Thread.currentThread();
        //keep going as long as the thread is alive
        while (t == gameloop) {
            try {
                Thread.sleep(1000 / FPS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //update the game loop
            gameUpdate();
            repaint();
        }
    }

    /**
     * updates all the game objects
     */
    private void gameUpdate() {
        switch (state) {
            case GameState.PLAYING:
                // update the pipes' position
                pipeUpdate();

                // update the birds' position
                birdUpdate();

                // check for collisions
                checkCollisions();
                break;
            case GameState.DEATH:
                onDeath();
                break;
            default:
                break;
        }
    }

    /**
     * update the pipes' positions
     */
    private void pipeUpdate() {
        // iterate through all the pipe objects
        for(int i = 0; i < MAXPIPESPRITES; i++) {
            // calculate the sprites new position
            double newXPos = topPipe[i].Position().X() + topPipe[i].Velocity().X();
            // offset it by the sprites width to calculate when it is offscreen
            double newXBoundary = newXPos + topPipe[i].imageWidth();


            if(newXBoundary <= 0) {
                // reset the pipes position
                topPipe[i].Position().setX(SCREENWIDTH);
                bottomPipe[i].Position().setX(SCREENWIDTH);

                // generate a new random y position for the pipe
                int randY = -rand.nextInt(320) - 64;

                // set the pipe's new y position
                topPipe[i].Position().setY(randY);

                // calculate the offset between the top and bottom pipes gap
                double offsetY = (topPipe[0].Position().Y() + topPipe[0].imageHeight()) + 128;

                // set the bottom pipes new y position
                bottomPipe[0].Position().setY(offsetY);

                // play the point sound effect
                point.play();

                // increment the score
                score++;
            }
        }

        // update the pipes' position
        for(int i = 0; i < MAXPIPESPRITES; i++) {
            // update the top pipe sprites' position
            topPipe[i].updatePosition();

            // update the bottom pipe sprites' position
            bottomPipe[i].updatePosition();
        }
    }

    /**
     * update the birds position
     */
    private void birdUpdate() {
        double newYPos = bird.Position().Y() + bird.Velocity().Y();

        // does the bird intersect the top boundary
        if(newYPos <= BIRDTOPBOUNDRY) {
            // if it does clip it so it's y position is on screen
            bird.Position().setY(BIRDTOPBOUNDRY);
        }

        // has the bird hit the death boundary
        if(newYPos >= BIRDDEATHBOUNDRY) {
            // clip the bird so it doesn't go off screen
            bird.Position().setY(BIRDDEATHBOUNDRY - bird.Velocity().Y());

            // set the game state to death
            state = GameState.DEATH;
        }

        // update the birds position
        bird.updatePosition();

        // apply gravity to the bird
        bird.Velocity().setY(GRAVITY);
    }

    /**
     * check for collisions
     */
    private void checkCollisions() {
        // check if the bird has collided with the top pipe
        for(int i= 0; i < MAXPIPESPRITES; i++) {
            // if the top pipe is active
            if(topPipe[i].alive()) {
                // if the bird and pipe has collided
                if(bird.collidesWith(topPipe[i])) {
                    // play the hit sound effect
                    hit.play();
                    // set the game state to game over
                    state = GameState.DEATH;
                }
            }

            // if the bottom pipe is alive
            if(bottomPipe[i].alive()) {
                // if the bird and pipe has collided
                if(bird.collidesWith(bottomPipe[i])) {
                    // play the hit sound effect
                    hit.play();
                    // set the game state to game over
                    state = GameState.DEATH;
                }
            }
        }
    }

    /**
     * update the high score
     */
    private void checkForHighScore() {
        // if the players score is greater then the high score
        if (score > hiscore) {
            hiscore = score;
            highscoredata.writeInteger(hiscore);
        }
    }

    /**
     * run when a new game is started
     */
    private void onStart() {
        // set all the pipes off screen
        for(int i = 0; i < MAXPIPESPRITES; i++) {
            topPipe[i].Position().setX(SCREENWIDTH);
            bottomPipe[i].Position().setX(SCREENWIDTH);
        }

        // generate a random y position for the pipe
        int randY = -rand.nextInt(320) - 64;

        // set the 1st top pipes' position
        topPipe[0].setPosition(new Point2D(SCREENWIDTH, randY));
        topPipe[0].setAlive(true);

        // calculate the offset between the top and bottom pipes gap
        double offsetY = (topPipe[0].Position().Y() + topPipe[0].imageHeight()) + 128;

        // set the 1st bottom pipes' position
        bottomPipe[0].setPosition(new Point2D(SCREENWIDTH, offsetY));
        bottomPipe[0].setAlive(true);

        // start moving the pipes
        topPipe[0].Velocity().setX(PIPEXVELOCITY);
        bottomPipe[0].Velocity().setX(PIPEXVELOCITY);

        // set the bird back to it's starting position
        bird.setPosition(new Point2D(SCREENCENTERX - bird.imageWidth(), SCREENCENTERY - bird.imageHeight()));

        // reset the score
        score = 0;

        // set the game state to playing
        state = GameState.PLAYING;
    }

    /**
     * run when the player dies
     */
    private void onDeath() {
        // zero out all the pipes velocity
        for(int i = 0; i < MAXPIPESPRITES; i++) {
            topPipe[i].Velocity().setX(0);
            bottomPipe[i].Velocity().setX(0);
        }
        // zero the birds velocity
        bird.Velocity().setY(0);

        // play the death sound effect
        death.play();

        // check if the player got a high score
        checkForHighScore();

        // set the game state to game over
        state = GameState.GAMEOVER;
    }

    /**
     * draws all the game entities to the back buffer and then the back buffer to the screen
     * @param g graphics context to use for drawing
     */
    public void paint(Graphics g) {
        // get a graphics context for the backbuffer
        Graphics backbufferGraphics = backbuffer.getGraphics();

        // clear the screen
        backbufferGraphics.setColor(new Color(0x87ceeb));
        backbufferGraphics.fillRect(0, 0, getWidth(), getHeight());

        // draw the background
        background.draw(backbufferGraphics);

        // set the color to white
        backbufferGraphics.setColor(new Color(0xffffff));
        // if the font hasn't been instanced
        if (font == null) {
            // create an instance of the font for the score, plain with a font size of 36
            font = new Font(null, Font.PLAIN, 36);
            // set the font
            backbufferGraphics.setFont(font);
        }

        switch(state) {
            case GameState.TITLESCREEN:
                // draw the hi score string
                backbufferGraphics.drawString("Hi Score",SCREENWIDTH - 120,20);

                // draw the actual high score
                backbufferGraphics.drawString(String.valueOf(hiscore),SCREENWIDTH - 90,40);

                // draw the title string
                backbufferGraphics.drawString("Blubird",SCREENCENTERX,200);

                // draw the press start string
                backbufferGraphics.drawString("Press Enter",SCREENCENTERX,300);
                break;
            case GameState.PLAYING:
            case GameState.DEATH:
                // draw the player sprite
                bird.draw(backbufferGraphics);

                //draw the pipes
                drawPipes(backbufferGraphics);

                // draw the players score
                backbufferGraphics.drawString(String.valueOf(score), 550, 50);
                break;
            case GameState.GAMEOVER:
                // draw the player sprite
                bird.draw(backbufferGraphics);

                //draw the pipes
                drawPipes(backbufferGraphics);

                // draw the players score
                backbufferGraphics.drawString(String.valueOf(score), 550, 50);

                // draw the game over string
                backbufferGraphics.drawString("Game Over",SCREENCENTERX,200);

                // draw the press start string
                backbufferGraphics.drawString("Press Enter",SCREENCENTERX,300);
                break;
        }
        // draw buffer on screen
        g.drawImage(backbuffer, 0, 0, null);
    }

    /**
     * draws the pipes
     *
     * @param backbuffer backbuffer image
     */
    private void drawPipes(Graphics backbuffer) {
        // draw the top pipes
        for(int i = 0; i < MAXPIPESPRITES; i++) {
            if (topPipe[i].alive()) {
                topPipe[i].draw(backbuffer);
            }

            //draw the bottom pipes
            if (bottomPipe[i].alive()) {
                bottomPipe[i].draw(backbuffer);
            }
        }
    }

    /**
 * listens for key events and handles them
 * @param evt key event
 */
    public void userEventReceived(UserEvent evt) {
        // if a key press event occurred
        if (evt.getType() == HRcEvent.KEY_PRESSED) {
            // if the enter key was pressed
            if (evt.getCode() == HRcEvent.VK_ENTER) {
                switch(state) {
                    case GameState.TITLESCREEN:
                        // start a new game
                        onStart();
                        break;
                    case GameState.PLAYING:
                        // play the wing flap sound effect
                        wingFlap.play();
                        // set the velocity to propel the bird upward
                        bird.Velocity().setY(UPFORCEY);
                        break;
                    case GameState.GAMEOVER:
                        state = GameState.TITLESCREEN;
                        break;
                }
            }
        }
    }
}
