package com.brackeen.javagamebook.tilegame;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.util.Iterator;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;


import com.brackeen.javagamebook.graphics.*;
import com.brackeen.javagamebook.sound.*;
import com.brackeen.javagamebook.input.*;
import com.brackeen.javagamebook.tilegame.sprites.*;
import com.brackeen.javagamebook.state.*;

public class MainGameState implements GameState {

    private static final int DRUM_TRACK = 1;

    public static final float GRAVITY = 0.002f;


    private SoundManager soundManager;
    private MidiPlayer midiPlayer;
    private TileGameResourceManager resourceManager;
    private int width;
    private int height;

    private Point pointCache = new Point();
    private Sound prizeSound;
    private Sound boopSound;
    private Sequence music;
    private TileMap map;
    private TileMapRenderer renderer;

    private String stateChange;

    private GameAction moveLeft;
    private GameAction moveRight;
    private GameAction jump;
    private GameAction exit;

    public MainGameState(SoundManager soundManager,
        MidiPlayer midiPlayer, int width, int height)
    {
        this.soundManager = soundManager;
        this.midiPlayer = midiPlayer;
        this.width = width;
        this.height = height;
        moveLeft = new GameAction("moveLeft");
        moveRight = new GameAction("moveRight");
        jump = new GameAction("jump",
            GameAction.DETECT_INITAL_PRESS_ONLY);
        exit = new GameAction("exit",
            GameAction.DETECT_INITAL_PRESS_ONLY);

        renderer = new TileMapRenderer();
        toggleDrumPlayback();
    }

    public String getName() {
        return "Main";
    }


    public String checkForStateChange() {
        return stateChange;
    }

    public void loadResources(ResourceManager resManager) {

        resourceManager = (TileGameResourceManager)resManager;

        resourceManager.loadResources();

        renderer.setBackground(
            resourceManager.loadImage("background.png"));

        // wczytaj pierwszą mapę
        map = resourceManager.loadNextMap();

        // wczytaj dźwięki
        prizeSound = resourceManager.loadSound("sounds/prize.wav");
        boopSound = resourceManager.loadSound("sounds/boop2.wav");
        music = resourceManager.loadSequence("sounds/music.midi");
    }

    public void start(InputManager inputManager) {
        inputManager.mapToKey(moveLeft, KeyEvent.VK_LEFT);
        inputManager.mapToKey(moveRight, KeyEvent.VK_RIGHT);
        inputManager.mapToKey(jump, KeyEvent.VK_SPACE);
        inputManager.mapToKey(jump, KeyEvent.VK_UP);
        inputManager.mapToKey(exit, KeyEvent.VK_ESCAPE);

        soundManager.setPaused(false);
        midiPlayer.setPaused(false);
        midiPlayer.play(music, true);
        toggleDrumPlayback();
    }

    public void stop() {
        soundManager.setPaused(true);
        midiPlayer.setPaused(true);
    }


    public void draw(Graphics2D g) {
        renderer.draw(g, map, width, height);
    }


    /**
        Włącza i wyłącza perkusję w muzyce midi (ścieżka 1).
    */
    public void toggleDrumPlayback() {
        Sequencer sequencer = midiPlayer.getSequencer();
        if (sequencer != null) {
            sequencer.setTrackMute(DRUM_TRACK,
                !sequencer.getTrackMute(DRUM_TRACK));
        }
    }

    private void checkInput(long elapsedTime) {

        if (exit.isPressed()) {
            stateChange = GameStateManager.EXIT_GAME;
            return;
        }

        Player player = (Player)map.getPlayer();
        if (player.isAlive()) {
            float velocityX = 0;
            if (moveLeft.isPressed()) {
                velocityX-=player.getMaxSpeed();
            }
            if (moveRight.isPressed()) {
                velocityX+=player.getMaxSpeed();
            }
            if (jump.isPressed()) {
                player.jump(false);
            }
            player.setVelocityX(velocityX);
        }

    }


    /**
        Zwraca kafelek kolidujący z duszkiem. Zmienione powinna
        być albo współrzędna x, albo y, nigdy obie jednocześnie.
        Zwraca null, jeśli nie wykryto żadnych kolizji.
    */
    public Point getTileCollision(Sprite sprite,
        float newX, float newY)
    {
        float fromX = Math.min(sprite.getX(), newX);
        float fromY = Math.min(sprite.getY(), newY);
        float toX = Math.max(sprite.getX(), newX);
        float toY = Math.max(sprite.getY(), newY);

        // uzyskuje współrzędne kafelka
        int fromTileX = TileMapRenderer.pixelsToTiles(fromX);
        int fromTileY = TileMapRenderer.pixelsToTiles(fromY);
        int toTileX = TileMapRenderer.pixelsToTiles(
            toX + sprite.getWidth() - 1);
        int toTileY = TileMapRenderer.pixelsToTiles(
            toY + sprite.getHeight() - 1);

        // sprawdza każdy kafelek w poszukiwaniu kolizji
        for (int x=fromTileX; x<=toTileX; x++) {
            for (int y=fromTileY; y<=toTileY; y++) {
                if (x < 0 || x >= map.getWidth() ||
                    map.getTile(x, y) != null)
                {
                    // kolizja znaleziona, zwraca kafelek
                    pointCache.setLocation(x, y);
                    return pointCache;
                }
            }
        }

        // nie wykryto żadnych kolizji
        return null;
    }


    /**
        Sprawdza, czy dwa sprajty ze sobą nie kolidują. Zwraca
        false, jeśli te dwa wskazane sprajty to ten sam sprajt.
        Zwraca false, jeśli przynajmniej jeden z podanych sprajtów
        jest martwy.
    */
    public boolean isCollision(Sprite s1, Sprite s2) {
        // jeśli jest to ten sam sprajt, zwróć false
        if (s1 == s2) {
            return false;
        }

        // jeśli jeden ze sprajtów jest martwy, zwróć false
        if (s1 instanceof Creature && !((Creature)s1).isAlive()) {
            return false;
        }
        if (s2 instanceof Creature && !((Creature)s2).isAlive()) {
            return false;
        }

        // zwraca położenie pikseli sprajtów
        int s1x = Math.round(s1.getX());
        int s1y = Math.round(s1.getY());
        int s2x = Math.round(s2.getX());
        int s2y = Math.round(s2.getY());

        // sprawdza, czy otoczenia obu sprajtów mają punkty przecięcia
        return (s1x < s2x + s2.getWidth() &&
            s2x < s1x + s1.getWidth() &&
            s1y < s2y + s2.getHeight() &&
            s2y < s1y + s1.getHeight());
    }


    /**
        Zwraca sprajt kolidujący ze wskazanym sprajtem lub wartość,
        null, jeśli z dany sprajtem nie koliduje żaden inny sprajt.
    */
    public Sprite getSpriteCollision(Sprite sprite) {

        // przeszukaj listę sprajtów
        Iterator i = map.getSprites();
        while (i.hasNext()) {
            Sprite otherSprite = (Sprite)i.next();
            if (isCollision(sprite, otherSprite)) {
                // znaleziono kolizję, zwróć kolidującego sprajta
                return otherSprite;
            }
        }

        // nie wykryto żanych kolizji
        return null;
    }


    /**
        Aktualizuje animację, położenie i własności ruchu wszystkich
        sprajtów w bieżącej mapie.
    */
    public void update(long elapsedTime) {
        Creature player = (Creature)map.getPlayer();


        // gracz nie żyje! wczytaj mapę od nowa
        if (player.getState() == Creature.STATE_DEAD) {
            map = resourceManager.reloadMap();
            return;
        }

        // uzyskaj wejście z klawiatury/myszy
        checkInput(elapsedTime);

        // aktualizuj gracza
        updateCreature(player, elapsedTime);
        player.update(elapsedTime);

        // aktualizuj pozostałe sprajty
        Iterator i = map.getSprites();
        while (i.hasNext()) {
            Sprite sprite = (Sprite)i.next();
            if (sprite instanceof Creature) {
                Creature creature = (Creature)sprite;
                if (creature.getState() == Creature.STATE_DEAD) {
                    i.remove();
                }
                else {
                    updateCreature(creature, elapsedTime);
                }
            }
            // normalna aktualizacja
            sprite.update(elapsedTime);
        }
    }


    /**
        Aktualizuje ten obiekt, uwzględnia grawitację w przypadku obiektów,
        które nie latają, sprawdza występowanie kolizji.
    */
    private void updateCreature(Creature creature,
        long elapsedTime)
    {

        // uwzględnij siłę grawitacji
        if (!creature.isFlying()) {
            creature.setVelocityY(creature.getVelocityY() +
                GRAVITY * elapsedTime);
        }

        // zmień x
        float dx = creature.getVelocityX();
        float oldX = creature.getX();
        float newX = oldX + dx * elapsedTime;
        Point tile =
            getTileCollision(creature, newX, creature.getY());
        if (tile == null) {
            creature.setX(newX);
        }
        else {
            if (dx > 0) {
                creature.setX(
                    TileMapRenderer.tilesToPixels(tile.x) -
                    creature.getWidth());
            }
            else if (dx < 0) {
                creature.setX(
                    TileMapRenderer.tilesToPixels(tile.x + 1));
            }
            creature.collideHorizontal();
        }
        if (creature instanceof Player) {
            checkPlayerCollision((Player)creature, false);
        }

        // zmień y
        float dy = creature.getVelocityY();
        float oldY = creature.getY();
        float newY = oldY + dy * elapsedTime;
        tile = getTileCollision(creature, creature.getX(), newY);
        if (tile == null) {
            creature.setY(newY);
        }
        else {
            if (dy > 0) {
                creature.setY(
                    TileMapRenderer.tilesToPixels(tile.y) -
                    creature.getHeight());
            }
            else if (dy < 0) {
                creature.setY(
                    TileMapRenderer.tilesToPixels(tile.y + 1));
            }
            creature.collideVertical();
        }
        if (creature instanceof Player) {
            boolean canKill = (oldY < creature.getY());
            checkPlayerCollision((Player)creature, canKill);
        }

    }


    /**
        Sprawdza, czy gracz nie koliduje z innymi sprajtami. Jeśli
        canKill ma wartość true, kolizje ze sprajtami będą powodowały
        ich śmierć.
    */
    public void checkPlayerCollision(Player player,
        boolean canKill)
    {
        if (!player.isAlive()) {
            return;
        }

        // sprawdza, czy gracz nie koliduje z innymi sprajtami
        Sprite collisionSprite = getSpriteCollision(player);
        if (collisionSprite instanceof PowerUp) {
            acquirePowerUp((PowerUp)collisionSprite);
        }
        else if (collisionSprite instanceof Creature) {
            Creature badguy = (Creature)collisionSprite;
            if (canKill) {
                // zabija przeciwnika i przyznaje graczowi nagrodę
                soundManager.play(boopSound);
                badguy.setState(Creature.STATE_DYING);
                player.setY(badguy.getY() - player.getHeight());
                player.jump(true);
            }
            else {
                // gracz ginie!
                player.setState(Creature.STATE_DYING);
            }
        }
    }


    /**
        Przyznaje graczowi wskazany obiekt klasy PowerUp i usuwa ten
        obiekt z mapy.
    */
    public void acquirePowerUp(PowerUp powerUp) {
        // usuwa PowerUp z mapy
        map.removeSprite(powerUp);

        if (powerUp instanceof PowerUp.Star) {
            // zrób tutaj cokolwiek, np. przyznaj graczowi dodatkowe punkty
            soundManager.play(prizeSound);
        }
        else if (powerUp instanceof PowerUp.Music) {
            // zmień muzykę
            soundManager.play(prizeSound);
            toggleDrumPlayback();
        }
        else if (powerUp instanceof PowerUp.Goal) {
            // przejdź do kolejnej mapy
            soundManager.play(prizeSound,
                new EchoFilter(2000, .7f), false);
            map = resourceManager.loadNextMap();
        }
    }

}