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 javax.sound.sampled.AudioFormat;

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

/**
    Obiekt GameManager zarządza wszystkimi częściami gry.
*/
public class GameManager extends GameCore {

    public static void main(String[] args) {
        new GameManager().run();
    }

    // nieskompresowany, 44 100 Hz, 16-bitowy, mono, ze znakiem
    // rosnąca kolejność bitów
    private static final AudioFormat PLAYBACK_FORMAT =
        new AudioFormat(44100, 16, 1, true, false);

    private static final int DRUM_TRACK = 1;

    public static final float GRAVITY = 0.002f;

    private Point pointCache = new Point();
    private TileMap map;
    private MidiPlayer midiPlayer;
    private SoundManager soundManager;
    private ResourceManager resourceManager;
    private Sound prizeSound;
    private Sound boopSound;
    private InputManager inputManager;
    private TileMapRenderer renderer;

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


    public void init() {
        super.init();

        // konfiguracja zarządcy danych wejściowych
        initInput();

        // uruchomienie zarządcy zasobów
        resourceManager = new ResourceManager(
        screen.getFullScreenWindow().getGraphicsConfiguration());

        // ładowanie zasobów
        renderer = new TileMapRenderer();
        renderer.setBackground(
            resourceManager.loadImage("background.png"));

        // ładowanie pierwszej mapy
        map = resourceManager.loadNextMap();

        // ładowanie dźwieków
        soundManager = new SoundManager(PLAYBACK_FORMAT);
        prizeSound = soundManager.getSound("sounds/prize.wav");
        boopSound = soundManager.getSound("sounds/boop2.wav");

        // odtwarzanie muzyki
        midiPlayer = new MidiPlayer();
        Sequence sequence =
            midiPlayer.getSequence("sounds/music.midi");
        midiPlayer.play(sequence, true);
        toggleDrumPlayback();
    }

    /**
        Zamknięcie wszystkich zasobów wykorzystywanych przez GameManager.
    */
    public void stop() {
        super.stop();
        midiPlayer.close();
        soundManager.close();
    }

    private void initInput() {
        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);

        inputManager = new InputManager(
            screen.getFullScreenWindow());
        inputManager.setCursor(InputManager.INVISIBLE_CURSOR);

        inputManager.mapToKey(moveLeft, KeyEvent.VK_LEFT);
        inputManager.mapToKey(moveRight, KeyEvent.VK_RIGHT);
        inputManager.mapToKey(jump, KeyEvent.VK_SPACE);
        inputManager.mapToKey(exit, KeyEvent.VK_ESCAPE);
    }

    private void checkInput(long elapsedTime) {

        if (exit.isPressed()) {
            stop();
        }

        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);
        }

    }

    public void draw(Graphics2D g) {
        renderer.draw(g, map,
            screen.getWidth(), screen.getHeight());
    }

    /**
        Zwraca bieżącą mapę.
    */
    public TileMap getMap() {
        return map;
    }

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

    /**
        Zawraca kafelek z którym Sprite wszedł w kolizję. Może myć zmieniana
        współrzedna X lub Y obiektu Sprite, a nie obie na raz. Zwraca null
        jeżeli nie zostanie wykryta kolizja.
    */
    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);

        // pobranie położenia
        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);

        // sprawdzenie kolizji z wszystkimi kafelkami
        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)
                {
                    // znaleziona kolizja, zwracanie kafelka
                    pointCache.setLocation(x, y);
                    return pointCache;
                }
            }
        }

        // nie znaleziona kolizja
        return null;
    }

    /**
        Sprawdza, czy dwa obiekty Sprite kolidują ze sobą. Zwraca false
        jeżeli te dwa obiekty są te same. Zwraca false jeżeli jeden z obiektów
        Sprite jest martwym obiektem Creature.
    */
    public boolean isCollision(Sprite s1, Sprite s2) {
        // jeżeli obiekty są takie same, zwraca false
        if (s1 == s2) {
            return false;
        }

        // jeżeli jeden z obiektów to martwy obiekt Creature, zwraca false
        if (s1 instanceof Creature && !((Creature)s1).isAlive()) {
            return false;
        }
        if (s2 instanceof Creature && !((Creature)s2).isAlive()) {
            return false;
        }

        // Pobranie położenia obiektów Sprite
        int s1x = Math.round(s1.getX());
        int s1y = Math.round(s1.getY());
        int s2x = Math.round(s2.getX());
        int s2y = Math.round(s2.getY());

        // sprawdzenie, czy granice duszków przecinają się
        return (s1x < s2x + s2.getWidth() &&
            s2x < s1x + s1.getWidth() &&
            s1y < s2y + s2.getHeight() &&
            s2y < s1y + s1.getHeight());
    }

    /**
        Zwraca obiekt Sprite, który koliduje z podanym obiektem Sprite,
        lub null gdy nie ma duszków kolidujących z podanym obiektem.
    */
    public Sprite getSpriteCollision(Sprite sprite) {

        // przeglądanie listy obiektów Sprite
        Iterator i = map.getSprites();
        while (i.hasNext()) {
            Sprite otherSprite = (Sprite)i.next();
            if (isCollision(sprite, otherSprite)) {
                // znaleziona kolizja, zwracanie obiektu Sprite
                return otherSprite;
            }
        }

        // nie znaleziona kolizja
        return null;
    }

    /**
        Aktualizacja animacji, położenia i prędkości wszystkich duszków
        z bieżącej mapy.
    */
    public void update(long elapsedTime) {
        Creature player = (Creature)map.getPlayer();

        // bohater nie żyje, uruchomienie mapy od nowa
        if (player.getState() == Creature.STATE_DEAD) {
            map = resourceManager.reloadMap();
            return;
        }

        // odczytanie stanu klawiatury i myszy
        checkInput(elapsedTime);

        // aktualizacja bohatera
        updateCreature(player, elapsedTime);
        player.update(elapsedTime);

        // katualizacja pozostałych duszków
        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);
        }
    }

    /**
        Aktualizacja położenia wrogów, dodawanie grawitacji do wrogów
        którzy nie latają oraz sprawdzanie kolizji.
    */
    private void updateCreature(Creature creature,
        long elapsedTime)
    {
        // dodawanie grawitacji
        if (!creature.isFlying()) {
            creature.setVelocityY(creature.getVelocityY() +
                GRAVITY * elapsedTime);
        }

        // zmiana współrzędnej 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 {
            // wyrównanie do granicy kafelka
            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);
        }

        // zmiana współrzędnej 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 {
            // wyrównanie do granicy kafelka
            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);
        }
    }

    /**
        Sprawdzenie kolizji gracza z innymi duszkami. Jeżeli
        zmienna canKill ma wartość true, kolizja z obiektem Creature
        powoduje ich śmierć.
    */
    public void checkPlayerCollision(Player player,
        boolean canKill)
    {
        if (!player.isAlive()) {
            return;
        }

        // sprawdzenie kolizji gracza z innymi duszkami
        Sprite collisionSprite = getSpriteCollision(player);
        if (collisionSprite instanceof PowerUp) {
            acquirePowerUp((PowerUp)collisionSprite);
        }
        else if (collisionSprite instanceof Creature) {
            Creature badguy = (Creature)collisionSprite;
            if (canKill) {
                // zniszczenie wroga i odbicie się bohatera od niego
                soundManager.play(boopSound);
                badguy.setState(Creature.STATE_DYING);
                player.setY(badguy.getY() - player.getHeight());
                player.jump(true);
            }
            else {
                // bohater zginał!
                player.setState(Creature.STATE_DYING);
            }
        }
    }

    /**
        Daje bohaterowi określony bonus i usuwa go z mapy.
    */
    public void acquirePowerUp(PowerUp powerUp) {
        // usunięcie z mapy
        map.removeSprite(powerUp);

        if (powerUp instanceof PowerUp.Star) {
            // wykonaj tu dowolna akcję, na przykład dodaj punkty
            soundManager.play(prizeSound);
        }
        else if (powerUp instanceof PowerUp.Music) {
            // zmiana muzyki
            soundManager.play(prizeSound);
            toggleDrumPlayback();
        }
        else if (powerUp instanceof PowerUp.Goal) {
            // przejście do następnej mapy
            soundManager.play(prizeSound,
                new EchoFilter(2000, .7f), false);
            map = resourceManager.loadNextMap();
        }
    }
}
